Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -507,16 +507,22 @@ classDiagram

### Phase 1 — Unify the substrate

- [ ] Create `@xnetjs/trust` (zero-dep): `InstallProvenance`, `TrustTier`,
`SandboxKind`, `deriveTrustTier`, `requiresCapabilityReprompt`,
`sandboxForTier`, with tests.
- [ ] Re-export from `packages/labs/src/trust.ts` and
- [x] Create `@xnetjs/trust` (zero-dep MIT leaf): `InstallProvenance`,
`TrustTier`, `SandboxKind`, `deriveTrustTier`, `requiresCapabilityReprompt`,
`sandboxForTier`, with tests (`packages/trust/`).
- [x] Re-export from `packages/labs/src/trust.ts` and
`packages/plugins/src/ecosystem/provenance-trust.ts`; delete the duplicated
bodies; keep the public APIs stable.
bodies; keep the public APIs stable. _As-built: `LabInstallSource`/
`PluginTrustTier`/`InstallProvenance`/`SandboxKind` preserved as aliases of
the shared types; `LabTrustTier` (in `labs/runtime/types.ts`) aliased to the
shared `TrustTier`. labs (46) + plugins (452) suites unchanged & green._
- [ ] Add `packages/plugins/src/ecosystem/runtime.ts`: run user/marketplace-tier
plugin code on the labs `RuntimeLadder`; first-party stays host-realm.
_(deferred — needs the benchmark below + a port to avoid the `plugins→labs`
cycle, since labs already depends on plugins.)_
- [ ] Benchmark plugin activation + a representative editor interaction against
0184 budgets; gate the runtime switch on no regression.
0184 budgets; gate the runtime switch on no regression. _(deferred with the
runtime switch above.)_

### Phase 2 — AI drives the ecosystem

Expand Down Expand Up @@ -552,9 +558,10 @@ classDiagram

## Validation Checklist

- [ ] `@xnetjs/trust` is the single source of trust logic; `labs` and `plugins`
- [x] `@xnetjs/trust` is the single source of trust logic; `labs` and `plugins`
both consume it; the byte-identical mirror is gone; all existing trust tests
pass against the shared package.
pass against the shared package (trust 10, plugins 452, labs 46 — green;
typecheck/eslint/prettier/fallow clean).
- [ ] A user-tier plugin and a user-tier Lab run on the **same** ladder rung
(SES Worker); a marketplace one on the iframe rung; first-party stays
host-realm; no plugin activation/editor-latency regression vs 0184 budgets.
Expand Down
1 change: 1 addition & 0 deletions packages/labs/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
"dependencies": {
"@xnetjs/data": "workspace:*",
"@xnetjs/plugins": "workspace:*",
"@xnetjs/trust": "workspace:*",
"quickjs-emscripten": "^0.31.0",
"ses": "^1.10.0"
},
Expand Down
5 changes: 3 additions & 2 deletions packages/labs/src/runtime/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,9 +19,10 @@ export type LabRuntimeTier = 'sandbox' | 'app' | 'server'

/**
* Capability follows provenance, never self-declaration. Assigned by the host
* from where the Lab/extension came from — mirrors `WidgetTrustTier`.
* from where the Lab/extension came from — mirrors `WidgetTrustTier`. Aliased to
* the shared `@xnetjs/trust` `TrustTier` (0194) so labs and plugins agree.
*/
export type LabTrustTier = 'first-party' | 'user' | 'marketplace'
export type { TrustTier as LabTrustTier } from '@xnetjs/trust'

export type LabLogLevel = 'log' | 'info' | 'warn' | 'error'

Expand Down
61 changes: 11 additions & 50 deletions packages/labs/src/trust.ts
Original file line number Diff line number Diff line change
@@ -1,58 +1,19 @@
/**
* Trust derivation (exploration 0180).
* Trust derivation (explorations 0180, 0194).
*
* The load-bearing invariant of the whole extensibility story: a Lab/extension's
* trust tier follows its PROVENANCE — where it came from — never anything the
* code declares about itself. A Lab you (or an AI you approved) authored locally
* installs at the `user` tier; something pulled from a marketplace installs at
* the `marketplace` tier (and gets the iframe).
* code declares about itself. When a Lab/extension node SYNCS to another device,
* the receiver must RE-DERIVE the tier from its own local install action — never
* trust a tier carried in the synced payload. {@link deriveTrustTier} is that
* single choke point.
*
* Critically, when a Lab/extension node SYNCS to another device, the receiver
* must RE-DERIVE the tier from its own local install action — never trust a tier
* carried in the synced payload. {@link deriveTrustTier} is that single choke
* point.
* 0194 extracted this logic into the zero-dep `@xnetjs/trust` leaf (it was
* byte-for-byte duplicated in `@xnetjs/plugins`); this module re-exports it under
* the labs-local `LabInstallSource` name to preserve the labs public API.
*/

import type { LabTrustTier } from './runtime/types'
export { deriveTrustTier, requiresCapabilityReprompt } from '@xnetjs/trust'

export type LabInstallSource =
/** Bundled with the app. */
| 'builtin'
/** Authored in this workspace by the user. */
| 'authored'
/** Generated by an AI agent and explicitly approved by the user. */
| 'ai-generated'
/** Imported from a file/manifest the user pasted or opened. */
| 'imported'
/** Installed from a public marketplace. */
| 'marketplace'
/** Arrived via P2P sync from another device/peer. */
| 'synced'

/**
* Map an install source to a trust tier. Note that `synced` does NOT inherit
* any tier — it always lands at `user` and the host must re-confirm
* capabilities locally before activating (sync is not consent).
*/
export function deriveTrustTier(source: LabInstallSource): LabTrustTier {
switch (source) {
case 'builtin':
return 'first-party'
case 'authored':
case 'ai-generated':
case 'imported':
case 'synced':
return 'user'
case 'marketplace':
return 'marketplace'
}
}

/**
* Whether installing from `source` must re-prompt the user for the extension's
* capabilities before activation. True for anything not authored on this device
* in this session — especially synced nodes.
*/
export function requiresCapabilityReprompt(source: LabInstallSource): boolean {
return source !== 'builtin' && source !== 'authored'
}
/** Where a Lab/extension came from — the only input to its trust tier. */
export type { InstallProvenance as LabInstallSource } from '@xnetjs/trust'
1 change: 1 addition & 0 deletions packages/plugins/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@
"dependencies": {
"@xnetjs/abuse": "workspace:*",
"@xnetjs/core": "workspace:*",
"@xnetjs/trust": "workspace:*",
"@xnetjs/data": "workspace:*",
"acorn": "^8.15.0"
},
Expand Down
76 changes: 11 additions & 65 deletions packages/plugins/src/ecosystem/provenance-trust.ts
Original file line number Diff line number Diff line change
@@ -1,70 +1,16 @@
/**
* @xnetjs/plugins — install provenance → trust tier (exploration 0192).
* @xnetjs/plugins — install provenance → trust tier (explorations 0192, 0194).
*
* This mirrors `@xnetjs/labs`'s `deriveTrustTier`/`requiresCapabilityReprompt`
* (the canonical version for Labs/runtime code) but lives in `@xnetjs/plugins`
* so the plugin *install path* can consult it without `@xnetjs/plugins` taking a
* dependency on `@xnetjs/labs`. The two must agree: tier follows **provenance**
* (where the plugin came from), never anything the manifest declares about
* itself, and a `synced` plugin always re-derives locally — sync is not consent.
* This used to carry a byte-for-byte copy of `@xnetjs/labs`'s trust logic
* (mirrored in 0192 to avoid a `plugins → labs` dependency edge). 0194 extracted
* the shared logic into the zero-dep `@xnetjs/trust` leaf, which both packages
* now consume — so this module is a thin re-export that preserves the plugins
* public API (`InstallProvenance`, `PluginTrustTier`, `SandboxKind`, and the
* three functions).
*/

/** Where a plugin came from — the only input to its trust tier. */
export type InstallProvenance =
/** Bundled with the app. */
| 'builtin'
/** Authored in this workspace by the user. */
| 'authored'
/** Generated by an AI agent and explicitly approved by the user. */
| 'ai-generated'
/** Imported from a file/manifest the user pasted or opened. */
| 'imported'
/** Installed from a public marketplace. */
| 'marketplace'
/** Arrived via P2P sync from another device/peer. */
| 'synced'
export { deriveTrustTier, requiresCapabilityReprompt, sandboxForTier } from '@xnetjs/trust'
export type { InstallProvenance, SandboxKind } from '@xnetjs/trust'

/** The execution trust tier a plugin runs at. */
export type PluginTrustTier = 'first-party' | 'user' | 'marketplace'

/**
* Map install provenance to a trust tier. `synced` does NOT inherit any tier —
* it lands at `user` and must re-confirm capabilities locally before activating.
*/
export function deriveTrustTier(provenance: InstallProvenance): PluginTrustTier {
switch (provenance) {
case 'builtin':
return 'first-party'
case 'authored':
case 'ai-generated':
case 'imported':
case 'synced':
return 'user'
case 'marketplace':
return 'marketplace'
}
}

/**
* Whether installing from `provenance` must re-prompt for capability consent
* before activation. True for anything not authored on this device in this
* session — especially synced and marketplace plugins.
*/
export function requiresCapabilityReprompt(provenance: InstallProvenance): boolean {
return provenance !== 'builtin' && provenance !== 'authored'
}

/** The sandbox a tier maps to (mirrors the dashboard widget tiers). */
export type SandboxKind = 'host' | 'ses-worker' | 'iframe'

/** Map a trust tier to the sandbox its code should run in. */
export function sandboxForTier(tier: PluginTrustTier): SandboxKind {
switch (tier) {
case 'first-party':
return 'host'
case 'user':
return 'ses-worker'
case 'marketplace':
return 'iframe'
}
}
/** The execution trust tier a plugin runs at (alias of the shared `TrustTier`). */
export type { TrustTier as PluginTrustTier } from '@xnetjs/trust'
22 changes: 22 additions & 0 deletions packages/trust/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
# @xnetjs/trust

The shared provenance→trust-tier primitives for xNet's extensibility systems
(exploration 0194).

Trust follows **provenance** — where an extension came from — never anything the
code declares about itself. A `synced` extension always re-derives its tier
locally (sync is not consent).

```ts
import { deriveTrustTier, requiresCapabilityReprompt, sandboxForTier } from '@xnetjs/trust'

deriveTrustTier('marketplace') // 'marketplace' → sandboxForTier → 'iframe'
deriveTrustTier('authored') // 'user' → 'ses-worker'
deriveTrustTier('builtin') // 'first-party' → 'host'
requiresCapabilityReprompt('synced') // true
```

This package is the single source of truth consumed by `@xnetjs/plugins`
(ecosystem layer, 0192) and `@xnetjs/labs` (0180), which previously carried
byte-for-byte identical copies of this logic. It is intentionally zero-dependency
and policy-only (no runtime) so it never becomes a coupling magnet.
29 changes: 29 additions & 0 deletions packages/trust/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
{
"name": "@xnetjs/trust",
"version": "0.0.1",
"description": "Shared provenance→trust-tier primitives for xNet extensibility — the single source of truth consumed by @xnetjs/plugins and @xnetjs/labs.",
"license": "MIT",
"type": "module",
"main": "./dist/index.js",
"types": "./dist/index.d.ts",
"exports": {
".": {
"import": "./dist/index.js",
"types": "./dist/index.d.ts"
}
},
"scripts": {
"build": "tsup src/index.ts --format esm --dts",
"test": "vitest run",
"test:watch": "vitest",
"typecheck": "tsc --noEmit",
"clean": "rm -rf dist"
},
"devDependencies": {
"@types/node": "^20.0.0",
"tsup": "^8.0.0",
"typescript": "^5.4.0",
"vitest": "^4.0.0"
},
"private": true
}
57 changes: 57 additions & 0 deletions packages/trust/src/index.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
/**
* Tests for @xnetjs/trust — the shared provenance→trust primitives (0194).
*/

import { describe, it, expect } from 'vitest'
import {
deriveTrustTier,
requiresCapabilityReprompt,
sandboxForTier,
type InstallProvenance
} from './index'

const ALL: InstallProvenance[] = [
'builtin',
'authored',
'ai-generated',
'imported',
'marketplace',
'synced'
]

describe('deriveTrustTier', () => {
it('maps provenance to tier; only builtin is first-party, only marketplace is marketplace', () => {
expect(deriveTrustTier('builtin')).toBe('first-party')
expect(deriveTrustTier('marketplace')).toBe('marketplace')
for (const p of ['authored', 'ai-generated', 'imported', 'synced'] as InstallProvenance[]) {
expect(deriveTrustTier(p)).toBe('user')
}
})

it('never throws and always returns a valid tier for any provenance', () => {
for (const p of ALL) {
expect(['first-party', 'user', 'marketplace']).toContain(deriveTrustTier(p))
}
})
})

describe('requiresCapabilityReprompt', () => {
it.each(ALL)('is false only for builtin/authored (%s)', (provenance) => {
const localToThisDevice = provenance === 'builtin' || provenance === 'authored'
expect(requiresCapabilityReprompt(provenance)).toBe(!localToThisDevice)
})
})

describe('sandboxForTier', () => {
it('maps tiers to sandboxes', () => {
expect(sandboxForTier('first-party')).toBe('host')
expect(sandboxForTier('user')).toBe('ses-worker')
expect(sandboxForTier('marketplace')).toBe('iframe')
})

it('round-trips: a marketplace install ends up in the iframe', () => {
expect(sandboxForTier(deriveTrustTier('marketplace'))).toBe('iframe')
expect(sandboxForTier(deriveTrustTier('synced'))).toBe('ses-worker')
expect(sandboxForTier(deriveTrustTier('builtin'))).toBe('host')
})
})
Loading
Loading