@cef-ai/widget-runtime 1.1.1 → 2.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -91,7 +91,7 @@ something the widget never sent.
91
91
  | Message | Direction | Fields |
92
92
  | --- | --- | --- |
93
93
  | `cef-widget:identity-request` | widget → host | `requestId: string`, `v: number` |
94
- | `cef-widget:identity-response` | host → widget | `requestId: string`, `v?: number`, `pubkey?: string`, `sigType?: 'ed25519' \| 'sr25519'` (default `ed25519`), `error?: string`, `errorCode?: 'unsupported-version'` |
94
+ | `cef-widget:identity-response` | host → widget | `requestId: string`, `v?: number`, `pubkey?: string`, `sigType?: 'ed25519' \| 'sr25519'` (default `ed25519`), `token?: string` (a bearer delegation the widget uses instead of asking you to sign), `context?: { runId?, recordId?, vaultId? }`, `error?: string`, `errorCode?: 'unsupported-version'` |
95
95
  | `cef-widget:sign-request` | widget → host | `requestId: string`, `v: number`, `bytes: number[]` |
96
96
  | `cef-widget:sign-response` | host → widget | `requestId: string`, `v?: number`, `signature: number[] \| null` (`null` = user declined), `error?: string`, `errorCode?: 'unsupported-version'` |
97
97
 
@@ -105,6 +105,15 @@ Rules:
105
105
  answers with `errorCode: 'unsupported-version'` — its own reply is still a v1
106
106
  message, so `v` itself cannot carry the rejection, and without the code the
107
107
  receiver would just see an opaque error string and keep retrying.
108
+ - **Which vault (`context.vaultId`).** By default a widget opens the
109
+ **caller's own** vault (`vault.ensure()`), and a `token` does not change that
110
+ — a delegation says *who*, not *which vault*. A workflow, meanwhile, runs in
111
+ the vault of the org that owns its agent service, so a host showing somebody
112
+ else's run must name that vault or the widget reads a different database and
113
+ reports it as empty. Optional: a host that sends none gets exactly today's
114
+ behaviour. A host that **does** send one and the widget cannot open it gets
115
+ `WidgetVaultUnreachableError` — the runtime refuses rather than falling back,
116
+ because a fallback is indistinguishable from a run that produced nothing.
108
117
  - **Sign verbatim.** The host must sign the exact bytes it receives with
109
118
  **`ed25519_signRaw`**. Do **not** use the high-level `signMessage` /
110
119
  `wallet_signMessage`, which wrap the payload in Substrate's
@@ -14,6 +14,9 @@
14
14
  // All hex values in the client-sdk shape are WITHOUT a 0x prefix, matching
15
15
  // the proven embed-wallet behaviour (see embed-adapter.ts history).
16
16
  import { hexToU8a } from '@polkadot/util';
17
+ import { encodeAddress } from '@polkadot/util-crypto';
18
+ /** Cere's registered SS58 prefix. */
19
+ const CERE_SS58_PREFIX = 54;
17
20
  // ── Internal helpers ──────────────────────────────────────────────────────────
18
21
  /** Encode bytes as lowercase hex WITHOUT a `0x` prefix. */
19
22
  function bytesToHexNo0x(bytes) {
@@ -36,10 +39,22 @@ function bytesToHexNo0x(bytes) {
36
39
  export function toVaultSdkSigner(pubkey, sigType, signFn) {
37
40
  return {
38
41
  type: sigType,
39
- // `address` is set to the hex pubkey (NOT an SS58 address). vault-sdk's
40
- // WalletAuthProvider reads `publicKey`/`type`, not `address`, to build the
41
- // auth headers — matching the toClientSdkWallet note below.
42
- address: pubkey,
42
+ /*
43
+ * A REAL SS58 ADDRESS, not the hex pubkey.
44
+ *
45
+ * It was the hex, justified by "vault-sdk's WalletAuthProvider reads
46
+ * `publicKey`/`type`, not `address`". True of that provider, and a trap: it
47
+ * made the field's correctness depend on which auth provider happened to be
48
+ * in use. The moment a widget carries a host delegation it uses
49
+ * `DelegationAuthProvider` instead, something downstream decodes `address`,
50
+ * and a hex string fails SS58's checksum — surfacing in the widget as
51
+ * "Something went wrong: Invalid checksum", which names neither the field
52
+ * nor the value.
53
+ *
54
+ * Encoding it costs one call and removes the dependency entirely: the
55
+ * address is right whoever reads it.
56
+ */
57
+ address: encodeAddress(hexToU8a(pubkey), CERE_SS58_PREFIX),
43
58
  publicKey: hexToU8a(pubkey),
44
59
  isReady: async () => true,
45
60
  sign: (bytes) => signFn(bytes),
package/dist/entry.js CHANGED
@@ -5,6 +5,7 @@ import { toVaultSdkSigner } from './bridge-wallet.js';
5
5
  import { connectStandaloneWallet } from './standalone-wallet.js';
6
6
  import { createIdentityResolver } from './resolve-identity.js';
7
7
  import { createVaultClient } from './vault-client.js';
8
+ import { toVaultSdkConfig } from './vault-sdk-config.js';
8
9
  import { createWidgetRuntime } from './runtime.js';
9
10
  import { captureAndUploadAudio, createVaultObjectUploader } from './audio/capture-upload.js';
10
11
  import { decodeToMono16k } from './audio/segment.js';
@@ -38,13 +39,35 @@ function boot() {
38
39
  bridge,
39
40
  connectWallet: () => connectStandaloneWallet(manifest.wallet),
40
41
  });
42
+ /** The host's delegation, captured when identity resolved. See resolveIdentity. */
43
+ let hostDelegation;
41
44
  const deps = {
42
45
  resolveIdentity: async () => {
43
46
  const id = await resolveSigningIdentity();
44
- return { source: id.source, publicKey: id.publicKey, sigType: id.sigType };
47
+ // Remembered for buildVaultClient, which is synchronous and runs after
48
+ // this. The resolver is memoised, so this is the same identity.
49
+ hostDelegation = id.token;
50
+ /*
51
+ * THE CONTEXT TRAVELS THE LAST STEP TOO.
52
+ *
53
+ * It was dropped here: the resolver put the host's `context` on the
54
+ * resolved identity, `createWidgetRuntime` reads `id.context` and hands
55
+ * it to `identity()` — and this literal, the only production
56
+ * implementation of `resolveIdentity`, rebuilt the object without it. So
57
+ * the handshake carried the run all the way to the runtime's front door
58
+ * and no widget could ever read it. Spread rather than re-listed, so the
59
+ * next key on the bag does not have to remember to come through here.
60
+ */
61
+ return {
62
+ source: id.source,
63
+ publicKey: id.publicKey,
64
+ sigType: id.sigType,
65
+ ...(id.context ? { context: id.context } : {}),
66
+ };
45
67
  },
46
68
  buildVaultClient: (m, id) => {
47
69
  const sigType = id.sigType ?? 'ed25519';
70
+ const hostToken = hostDelegation;
48
71
  // Defer to the memoized identity for signing — resolveIdentity() has
49
72
  // already run by the time buildVaultClient is called, so this resolves
50
73
  // to the same (host or wallet) signer without a second handshake/popup.
@@ -52,14 +75,22 @@ function boot() {
52
75
  const vaultSdkSigner = toVaultSdkSigner(id.publicKey, sigType, signFn);
53
76
  // vault-sdk SDK instance: used for vault.ensure(), cubby reads, publish,
54
77
  // connectAgent, agentStatus — the single SDK for the whole runtime.
55
- const makeSdk = () => new VaultSDK({
56
- endpoint: m.endpoints.vault,
57
- garEndpoint: m.endpoints.gar,
58
- marketplaceEndpoint: m.endpoints.marketplace,
59
- s3GatewayAuthInfoUrl: m.endpoints.s3GatewayAuthInfo,
60
- signer: vaultSdkSigner,
61
- });
62
- return createVaultClient(m, makeSdk);
78
+ // `toVaultSdkConfig` carries `endpoints.rpc` as `chainUrl`: ensure()'s
79
+ // wallet path grants vault-api an on-chain NonTransfer proxy before the
80
+ // tokenless claim and cannot run without a chain URL.
81
+ // THE HOST'S DELEGATION, WHEN IT GAVE ONE. `auth` overrides `signer`, so
82
+ // this handle structurally cannot sign — and on the passkey wallet that is
83
+ // the difference between one ceremony and one per request: a keyless
84
+ // session steps up on every signature, so a widget that polls a query
85
+ // asked for a passkey forever. `sign` stays wired for hosts that supply
86
+ // no token.
87
+ const makeSdk = () => new VaultSDK(toVaultSdkConfig(m.endpoints, vaultSdkSigner, hostToken));
88
+ // WHICH VAULT, when the host named one. It rides `context` rather than
89
+ // a field of its own because it is the same kind of fact as the run id —
90
+ // why this widget was opened — and because an optional key on an
91
+ // already-optional bag is the only shape that leaves a host which sends
92
+ // nothing behaving exactly as it does today.
93
+ return createVaultClient(m, makeSdk, hostToken, id.context?.vaultId);
63
94
  },
64
95
  };
65
96
  // Build the mount deps (audio uploader) for config-driven audio kinds.
@@ -85,13 +116,12 @@ function boot() {
85
116
  uploaderP = (async () => {
86
117
  const id = await resolveSigningIdentity();
87
118
  const vaultSdkSigner = toVaultSdkSigner(id.publicKey, id.sigType, id.sign);
88
- const sdk = new VaultSDK({
89
- endpoint: manifest.endpoints.vault,
90
- garEndpoint: manifest.endpoints.gar,
91
- marketplaceEndpoint: manifest.endpoints.marketplace,
92
- s3GatewayAuthInfoUrl: manifest.endpoints.s3GatewayAuthInfo,
93
- signer: vaultSdkSigner,
94
- });
119
+ // Same delegation preference as the vault client: an upload that signs
120
+ // per chunk on a keyless session is a ceremony per chunk.
121
+ // Same config builder as the main vault client above — in particular
122
+ // the same `chainUrl`, since this `ensure({ onboard: false })` runs
123
+ // the vault-delegate grant step too.
124
+ const sdk = new VaultSDK(toVaultSdkConfig(manifest.endpoints, vaultSdkSigner, id.token));
95
125
  const ensuredVault = await sdk.vault.ensure({ onboard: false });
96
126
  return createVaultObjectUploader({ objects: ensuredVault.scope(manifest.scope).objects });
97
127
  })().catch((err) => { uploaderP = null; throw err; });
@@ -127,9 +157,19 @@ function boot() {
127
157
  // script and are unaffected.)
128
158
  onDomReady(() => {
129
159
  const app = document.getElementById('app');
130
- if (app) {
131
- void import('./mount.js').then(({ mountKind }) => mountKind(app, manifest.config, window.WidgetRuntime, mountDeps));
160
+ if (!app) {
161
+ // SAY SO. This used to be a bare `if (app)`, so a widget whose entry
162
+ // HTML had no mount point loaded its runtime, resolved its identity,
163
+ // ran nothing, and rendered whatever static markup the file happened to
164
+ // contain. From the outside that is indistinguishable from a query
165
+ // returning no rows — the page is simply there, and wrong, with an
166
+ // empty console. Cost us a devnet round trip to find.
167
+ console.error(`[widget-runtime] widget "${manifest.widgetId}" declares kind "${manifest.kind}" ` +
168
+ 'but its entry HTML has no <div id="app"></div>, so there is nowhere to render. ' +
169
+ 'Add the element, or declare kind "custom" and mount it yourself.');
170
+ return;
132
171
  }
172
+ void import('./mount.js').then(({ mountKind }) => mountKind(app, manifest.config, window.WidgetRuntime, mountDeps));
133
173
  });
134
174
  }
135
175
  }
@@ -21,6 +21,44 @@ export declare const UNSUPPORTED_VERSION_CODE = "unsupported-version";
21
21
  /** Largest `bytes` payload a host will sign, and largest signature accepted. */
22
22
  export declare const MAX_SIGN_BYTES: number;
23
23
  export type SigType = VaultSigType;
24
+ /**
25
+ * What the host can tell a widget about WHY it was opened.
26
+ *
27
+ * Every field optional, and a widget may ignore any of them — see
28
+ * `IdentityResponse.context`. Additive only: a new key must never change what a
29
+ * widget that does not read it does.
30
+ */
31
+ export interface WidgetHostContext {
32
+ /** The workflow execution being viewed, when the widget was opened from one. */
33
+ runId?: string;
34
+ /** One record, when the widget was opened from a row rather than a run. */
35
+ recordId?: string;
36
+ /**
37
+ * WHICH VAULT THE RUN HAPPENED IN — and therefore which one to read.
38
+ *
39
+ * A widget authenticates as the viewer and then opened the VIEWER'S OWN
40
+ * vault, because `vault.ensure()` claims the caller's vault and a delegation
41
+ * says WHO, not WHICH. A workflow, meanwhile, runs in the vault of the org
42
+ * that owns its agent SERVICE. Those are the same database only when the
43
+ * viewer happens to own that org — so for a member of somebody else's
44
+ * service the widget read a different database, found nothing, and said so:
45
+ * "No candidate has been reviewed yet" about a run whose own INSERT had
46
+ * reported `rowsAffected: 1`. An empty answer and a wrong-database answer
47
+ * were indistinguishable.
48
+ *
49
+ * OPTIONAL, and that is the compatibility story — the same one `token` and
50
+ * `context` itself have. A host that sends none gets exactly today's
51
+ * `ensure()`, the viewer's own vault, so every already-deployed widget and
52
+ * every host that has not been taught this key is untouched.
53
+ *
54
+ * CONTEXT, NOT A COMMAND still holds, in the only sense it can here: a widget
55
+ * that reads nothing from `context` never notices this key. But the runtime
56
+ * acts on it before any widget code runs, and when it cannot open the named
57
+ * vault it REFUSES — see `WidgetVaultUnreachableError`. Falling back to the
58
+ * viewer's own vault is what produced the confident lie above.
59
+ */
60
+ vaultId?: string;
61
+ }
24
62
  export interface IdentityRequest {
25
63
  type: typeof IDENTITY_REQUEST_TYPE;
26
64
  requestId: string;
@@ -34,6 +72,42 @@ export interface IdentityResponse {
34
72
  pubkey?: string;
35
73
  /** Defaults to 'ed25519' when omitted. */
36
74
  sigType?: SigType;
75
+ /**
76
+ * A bearer delegation the widget may use INSTEAD of asking the host to sign.
77
+ *
78
+ * WHY IT EXISTS. Without one, every vault request a widget makes is signed —
79
+ * and on the passkey wallet a signature from a keyless session raises a
80
+ * step-up. A widget that polls a query therefore raised a passkey prompt per
81
+ * poll, forever. The host already holds a session and can mint a delegation
82
+ * with no ceremony at all, so handing one over turns N ceremonies into none.
83
+ *
84
+ * OPTIONAL, AND THAT IS THE COMPATIBILITY STORY. A host that does not send
85
+ * one gets exactly the old behaviour: the widget falls back to
86
+ * `cef-widget:sign-request` per operation. Nothing already deployed changes.
87
+ */
88
+ token?: string;
89
+ /**
90
+ * WHICH RUN THE HOST IS SHOWING — so a widget opened from an execution can
91
+ * show THAT execution rather than the latest.
92
+ *
93
+ * Asked for by name, and left undesigned until now: "we should be able to
94
+ * pass a parameter, so that the widget opens the right records … a simple
95
+ * 'id' parameter would suffice and then anyone who decides to build a widget
96
+ * can decide to support that parameter or not."
97
+ *
98
+ * That last clause is the design. This is CONTEXT, not a command: a widget
99
+ * that ignores it behaves exactly as it does today, and a host that sends
100
+ * none is the existing case. Neither side has to change for the other's to
101
+ * work, which is why it is a field on the handshake rather than a new
102
+ * message or a required query parameter.
103
+ *
104
+ * A BAG, not a bare string, and the reason is the thing it will grow into: an
105
+ * execution id is what an execution view has, but the same slot is where a
106
+ * record id belongs when a widget is opened from a row — and those are
107
+ * different keys, which nobody has yet decided are the same. Naming the field
108
+ * now costs nothing; conflating them would cost a migration.
109
+ */
110
+ context?: WidgetHostContext;
37
111
  error?: string;
38
112
  /** Machine-readable reason; see UNSUPPORTED_VERSION_CODE. */
39
113
  errorCode?: typeof UNSUPPORTED_VERSION_CODE;
@@ -73,12 +147,18 @@ export declare function isUnsupportedVersion(v: unknown): boolean;
73
147
  */
74
148
  export declare function isByteArray(value: unknown): value is number[];
75
149
  export interface HostBridge {
76
- requestIdentity: () => Promise<{
77
- pubkey: string;
78
- sigType: SigType;
79
- } | null>;
150
+ requestIdentity: () => Promise<HostIdentity | null>;
80
151
  sign: (bytes: Uint8Array) => Promise<Uint8Array>;
81
152
  }
153
+ /** What the host answered the handshake with. */
154
+ export interface HostIdentity {
155
+ pubkey: string;
156
+ sigType: SigType;
157
+ /** A bearer delegation, when the host supplied one. See IdentityResponse.token. */
158
+ token?: string;
159
+ /** Why the host opened this widget. See IdentityResponse.context. */
160
+ context?: WidgetHostContext;
161
+ }
82
162
  /**
83
163
  * Returns the host bridge client. The bridge is stateless across calls
84
164
  * (identity is cached internally after the first successful handshake).
@@ -175,7 +175,12 @@ async function requestIdentity() {
175
175
  scheduleRetryOrTimeout();
176
176
  return;
177
177
  }
178
- const result = { pubkey: d.pubkey, sigType: d.sigType ?? 'ed25519' };
178
+ const result = {
179
+ pubkey: d.pubkey,
180
+ sigType: d.sigType ?? 'ed25519',
181
+ ...(d.token ? { token: d.token } : {}),
182
+ ...(d.context ? { context: d.context } : {}),
183
+ };
179
184
  finish();
180
185
  cachedIdentity = result;
181
186
  outerResolve(result);
package/dist/index.d.ts CHANGED
@@ -12,6 +12,7 @@ export { renderSubmit } from './kinds/submit.js';
12
12
  export { renderConversation } from './kinds/conversation.js';
13
13
  export { renderComposite, registerCompositePanel, getCompositePanel, type CompositePanelRenderer } from './kinds/composite.js';
14
14
  export * from './render-helpers.js';
15
+ export { WidgetVaultUnreachableError } from './vault-client.js';
15
16
  export type { WidgetKind, WidgetConfig, RecordConfig, ListConfig, DashboardConfig, SubmitConfig, ConversationConfig, CustomPanelConfig, CompositePanel, CompositeConfig } from './types.js';
16
17
  export { captureAndUploadAudio, createVaultObjectUploader, createVaultObjectStore } from './audio/capture-upload.js';
17
18
  export type { CaptureUploadInput, CaptureUploadDeps, VaultObjectUploaderConfig, VaultObjectsLike, VaultStoreConfig, VaultObjectStore } from './audio/capture-upload.js';
@@ -21,6 +22,7 @@ export { encodeWav } from './audio/wav.js';
21
22
  export { segmentPcm, decodeToMono16k } from './audio/segment.js';
22
23
  export { requestMicStream } from './audio/mic.js';
23
24
  export { createHostBridge, type HostBridge } from './host-bridge.js';
25
+ export type { WidgetHostContext } from './host-bridge.js';
24
26
  export { IDENTITY_REQUEST_TYPE, IDENTITY_RESPONSE_TYPE, SIGN_REQUEST_TYPE, SIGN_RESPONSE_TYPE, PROTOCOL_VERSION, UNSUPPORTED_VERSION_CODE, MAX_SIGN_BYTES, isByteArray, WidgetProtocolVersionError, WidgetMalformedMessageError, } from './host-bridge.js';
25
27
  export type { SigType, IdentityRequest, IdentityResponse, SignRequest, SignResponse, } from './host-bridge.js';
26
28
  export { createWidgetHost, type WidgetHostOptions, type WidgetHostIdentity } from './widget-host.js';
package/dist/index.js CHANGED
@@ -11,6 +11,7 @@ export { renderSubmit } from './kinds/submit.js';
11
11
  export { renderConversation } from './kinds/conversation.js';
12
12
  export { renderComposite, registerCompositePanel, getCompositePanel } from './kinds/composite.js';
13
13
  export * from './render-helpers.js';
14
+ export { WidgetVaultUnreachableError } from './vault-client.js';
14
15
  export { captureAndUploadAudio, createVaultObjectUploader, createVaultObjectStore } from './audio/capture-upload.js';
15
16
  export { mintDelegationToken, Operation } from './audio/ddc-delegation.js';
16
17
  export { encodeWav } from './audio/wav.js';
@@ -20,7 +20,10 @@ export interface BuildManifestInput {
20
20
  queries?: Array<{
21
21
  id: string;
22
22
  label?: string;
23
- sql: string;
23
+ sql?: string;
24
+ cubby?: string;
25
+ tool?: 'search' | 'get' | 'neighbours' | 'countByType';
26
+ limit?: number;
24
27
  timeoutMs?: number;
25
28
  }>;
26
29
  events?: WidgetManifest['events'];
package/dist/manifest.js CHANGED
@@ -5,11 +5,23 @@ export function buildWidgetManifest(input) {
5
5
  // Build queries[]: explicit array takes priority; otherwise fall back to single query shorthand.
6
6
  let resolvedQueries;
7
7
  if (input.queries && input.queries.length > 0) {
8
+ // FIELD BY FIELD, and every new field has to be added here.
9
+ //
10
+ // This is the only copy of the manifest the runtime ever sees — it is baked
11
+ // into the widget's entry HTML as `window.WidgetSandbox.manifest` — so a
12
+ // field dropped here fails in the most expensive way available: the widget
13
+ // loads, the query runs, and it reads the wrong thing. Losing `tool` turns
14
+ // a memory read into a cubby read with no SQL; losing `cubby` sends a query
15
+ // to the widget's default alias and shows an empty table that looks like
16
+ // "nothing happened yet".
8
17
  resolvedQueries = input.queries.map((q) => ({
9
18
  id: q.id,
10
19
  label: q.label ?? q.id,
11
20
  sql: q.sql,
12
21
  timeoutMs: q.timeoutMs ?? 60000,
22
+ ...(q.cubby ? { cubby: q.cubby } : {}),
23
+ ...(q.tool ? { tool: q.tool } : {}),
24
+ ...(q.limit !== undefined ? { limit: q.limit } : {}),
13
25
  }));
14
26
  }
15
27
  else if (input.query) {
package/dist/mount.js CHANGED
@@ -27,6 +27,32 @@ export async function mountKind(root, config, runtime, deps) {
27
27
  await renderer(root, config, runtime, deps);
28
28
  }
29
29
  catch (err) {
30
+ /*
31
+ * THE RUN IS SOMEWHERE THIS WIDGET CANNOT LOOK — said here, once, rather
32
+ * than in each widget.
33
+ *
34
+ * This case used to arrive as zero rows, so every widget answered it with
35
+ * its own `empty` copy: "No candidate has been reviewed yet", about a run
36
+ * whose own INSERT had reported `rowsAffected: 1`. No widget can tell an
37
+ * empty result from a wrong-database result, so no widget should have to
38
+ * — the runtime is what knows which vault it could not open, and it is
39
+ * the only layer that can name it.
40
+ *
41
+ * Matched on `name`, like AgentNotConnectedError below, so an error that
42
+ * crossed a bundle boundary still lands here — widget-runtime ships both
43
+ * as this package and as a vendored IIFE.
44
+ */
45
+ if (err && err.name === 'WidgetVaultUnreachableError') {
46
+ const vaultId = err.vaultId;
47
+ root.innerHTML =
48
+ '<p class="error">This run executed in a vault this widget cannot read'
49
+ + (vaultId ? ` (<code>${esc(vaultId)}</code>)` : '')
50
+ + '.</p>'
51
+ + '<p class="muted">Nothing is shown, on purpose: your own vault is a different '
52
+ + 'database, and reading that one here would look exactly like a run that produced '
53
+ + 'nothing. Ask an admin of that organization for access.</p>';
54
+ return;
55
+ }
30
56
  if (err && err.name === 'AgentNotConnectedError') {
31
57
  root.innerHTML = `<p class="muted">Connect your account to view this.</p><button id="cta">Connect</button>`;
32
58
  root.querySelector('#cta').addEventListener('click', async () => {
@@ -1,4 +1,4 @@
1
- import type { HostBridge } from './host-bridge.js';
1
+ import type { HostBridge, WidgetHostContext } from './host-bridge.js';
2
2
  import type { StandaloneWallet } from './standalone-wallet.js';
3
3
  /**
4
4
  * A resolved signing identity plus its raw-bytes signer. Shared by both the
@@ -9,6 +9,26 @@ export interface ResolvedSigningIdentity {
9
9
  publicKey: string;
10
10
  sigType: 'ed25519' | 'sr25519';
11
11
  sign: (bytes: Uint8Array) => Promise<Uint8Array>;
12
+ /**
13
+ * A bearer delegation the host supplied, when it did.
14
+ *
15
+ * PREFER IT OVER `sign` FOR EVERY VAULT REQUEST. On the passkey wallet a
16
+ * signature from a keyless session raises a step-up, so a widget that signs
17
+ * each request raises a ceremony each request — a polling query asks forever.
18
+ * A delegation is minted once by the host, with no ceremony at all, and
19
+ * authenticates exactly the same reads and writes.
20
+ *
21
+ * `sign` stays for hosts that supply no token, and for anything a delegation
22
+ * genuinely cannot carry.
23
+ */
24
+ token?: string;
25
+ /**
26
+ * Why the host opened this widget — the run being viewed, or the record.
27
+ *
28
+ * CONTEXT, NOT A COMMAND. A widget that ignores it behaves exactly as it does
29
+ * today, which is the compatibility story for every widget already deployed.
30
+ */
31
+ context?: WidgetHostContext;
12
32
  }
13
33
  /**
14
34
  * The widget is framed by a host, but the host never supplied an identity —
@@ -36,6 +36,8 @@ export function createIdentityResolver(deps) {
36
36
  publicKey: hostId.pubkey,
37
37
  sigType: hostId.sigType,
38
38
  sign: (bytes) => deps.bridge.sign(bytes),
39
+ ...(hostId.token ? { token: hostId.token } : {}),
40
+ ...(hostId.context ? { context: hostId.context } : {}),
39
41
  };
40
42
  }
41
43
  // Framed but unanswered: the host owns the identity. Do not downgrade.
package/dist/runtime.d.ts CHANGED
@@ -1,9 +1,12 @@
1
1
  import type { WidgetManifest, WidgetRuntimeApi } from './types.js';
2
+ import type { WidgetHostContext } from './host-bridge.js';
2
3
  import type { VaultClient } from './vault-client.js';
3
4
  export interface ResolvedRuntimeIdentity {
4
5
  source: string;
5
6
  publicKey: string;
6
7
  sigType?: 'ed25519' | 'sr25519';
8
+ /** Why the host opened this widget — see `Identity.context`. */
9
+ context?: WidgetHostContext;
7
10
  }
8
11
  export interface RuntimeDeps {
9
12
  resolveIdentity: (manifest: WidgetManifest) => Promise<ResolvedRuntimeIdentity>;
package/dist/runtime.js CHANGED
@@ -1,11 +1,12 @@
1
1
  export function createWidgetRuntime(manifest, deps) {
2
2
  let status = 'anon';
3
3
  let publicKey = '';
4
+ let context;
4
5
  let clientP;
5
6
  const subs = new Set();
6
7
  const emit = (s) => {
7
8
  status = s;
8
- const snapshot = { publicKey, status };
9
+ const snapshot = { publicKey, status, ...(context ? { context } : {}) };
9
10
  for (const cb of subs)
10
11
  cb(snapshot);
11
12
  };
@@ -14,6 +15,16 @@ export function createWidgetRuntime(manifest, deps) {
14
15
  emit('connecting');
15
16
  const id = await deps.resolveIdentity(manifest);
16
17
  publicKey = id.publicKey;
18
+ /*
19
+ * THE RUN THE HOST OPENED THIS ON, kept so the widget can ask for it.
20
+ *
21
+ * It arrived on the handshake and stopped here: the resolver put it on
22
+ * the resolved identity and nothing carried it the last step into
23
+ * `identity()`. A widget could therefore be handed the execution it was
24
+ * opened for and have no way to read it — which is the entire reason the
25
+ * field exists.
26
+ */
27
+ context = id.context;
17
28
  const client = deps.buildVaultClient(manifest, id);
18
29
  emit('ready');
19
30
  return client;
@@ -27,8 +38,14 @@ export function createWidgetRuntime(manifest, deps) {
27
38
  return {
28
39
  async query(ref, params) { return (await ensureClient()).query(ref, params); },
29
40
  async publish(type, payload, context) { return (await ensureClient()).publish(type, payload, context); },
30
- async identity() { await ensureClient(); return { publicKey, status }; },
31
- async connect() { await ensureClient(); return { publicKey, status }; },
41
+ async identity() {
42
+ await ensureClient();
43
+ return { publicKey, status, ...(context ? { context } : {}) };
44
+ },
45
+ async connect() {
46
+ await ensureClient();
47
+ return { publicKey, status, ...(context ? { context } : {}) };
48
+ },
32
49
  async connectAgent() { await (await ensureClient()).connectAgent(); },
33
50
  async agentStatus() { return (await ensureClient()).agentStatus(); },
34
51
  onIdentityChange(cb) { subs.add(cb); return () => subs.delete(cb); },
package/dist/types.d.ts CHANGED
@@ -1,3 +1,51 @@
1
+ import type { WidgetHostContext } from './host-bridge.js';
2
+ /**
3
+ * One named read a widget may perform.
4
+ *
5
+ * TWO PLACES TO READ FROM, because a widget has two honest questions and only
6
+ * one of them used to be answerable:
7
+ *
8
+ * the AGENT SERVICE's cubbies — what this agent and its siblings did. A
9
+ * workflow runner, a coordinator and three specialists are one service; a
10
+ * widget that could only read one of them could only tell part of the story.
11
+ * There is no separate per-agent store any more: `ctx.cubby` addresses the
12
+ * service, so the widget's own agent's rows are already in there.
13
+ *
14
+ * the vault's MEMORY BANK — what the organisation concluded, which outlives
15
+ * every run.
16
+ *
17
+ * `sql` means the first and `tool` means the second, and the two are mutually
18
+ * exclusive. There is no `source` discriminator: beside two already-disjoint
19
+ * fields it would only be a way for them to disagree.
20
+ */
21
+ export interface WidgetQueryDecl {
22
+ id: string;
23
+ label: string;
24
+ /**
25
+ * SQL against one of the agent service's cubbies. Mutually exclusive with
26
+ * {@link tool}, and one of the two is what says where this query reads.
27
+ */
28
+ sql?: string;
29
+ /** Which of the service's cubbies. Defaults to the widget's `cubbyAlias`. */
30
+ cubby?: string;
31
+ /**
32
+ * A read from the vault's memory bank, named rather than expressed as SQL —
33
+ * the bank is a gated graph API, not a table, and a SQL string would imply a
34
+ * reach it does not offer.
35
+ *
36
+ * `search` takes the match string as the first parameter; `get` and
37
+ * `neighbours` take a record id; `countByType` takes none.
38
+ *
39
+ * THERE IS NO THIRD PLACE TO READ FROM, so there is no `source` field to
40
+ * choose one. A widget sees the agent service's cubbies and the vault's
41
+ * memory; `sql` means the first and `tool` means the second. A discriminator
42
+ * beside two already-disjoint fields is a way for them to disagree.
43
+ */
44
+ tool?: 'search' | 'get' | 'neighbours' | 'countByType';
45
+ /** `tool` only: row cap. The bank's own default applies when absent. */
46
+ limit?: number;
47
+ timeoutMs?: number;
48
+ }
1
49
  export interface WidgetManifest {
2
50
  schemaVersion: 1;
3
51
  widgetId: string;
@@ -16,12 +64,7 @@ export interface WidgetManifest {
16
64
  s3GatewayAuthInfo: string;
17
65
  rpc: string;
18
66
  };
19
- queries: Array<{
20
- id: string;
21
- label: string;
22
- sql: string;
23
- timeoutMs?: number;
24
- }>;
67
+ queries: WidgetQueryDecl[];
25
68
  events?: Array<{
26
69
  type: string;
27
70
  schemaRef?: string;
@@ -33,6 +76,17 @@ export type IdentityStatus = 'anon' | 'connecting' | 'ready';
33
76
  export interface Identity {
34
77
  publicKey: string;
35
78
  status: IdentityStatus;
79
+ /**
80
+ * WHY THE HOST OPENED THIS WIDGET — the run being viewed, or the record.
81
+ *
82
+ * Absent when the widget is standalone, or when the host said nothing, and
83
+ * ABSENT rather than empty: a widget doing `'context' in id` must be able to
84
+ * tell "the host told me nothing" from "the host told me something empty".
85
+ *
86
+ * This is what lets one widget serve two jobs: opened from the Widgets tab it
87
+ * shows the latest, opened from an execution it shows THAT execution.
88
+ */
89
+ context?: WidgetHostContext;
36
90
  }
37
91
  export interface QueryResult {
38
92
  columns: string[];