@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 +10 -1
- package/dist/bridge-wallet.js +19 -4
- package/dist/entry.js +58 -18
- package/dist/host-bridge.d.ts +84 -4
- package/dist/host-bridge.js +6 -1
- package/dist/index.d.ts +2 -0
- package/dist/index.js +1 -0
- package/dist/manifest.d.ts +4 -1
- package/dist/manifest.js +12 -0
- package/dist/mount.js +26 -0
- package/dist/resolve-identity.d.ts +21 -1
- package/dist/resolve-identity.js +2 -0
- package/dist/runtime.d.ts +3 -0
- package/dist/runtime.js +20 -3
- package/dist/types.d.ts +60 -6
- package/dist/vault-client.d.ts +96 -2
- package/dist/vault-client.js +172 -59
- package/dist/vault-sdk-config.d.ts +26 -0
- package/dist/vault-sdk-config.js +37 -0
- package/dist/widget-runtime.js +145 -140
- package/package.json +2 -2
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
|
package/dist/bridge-wallet.js
CHANGED
|
@@ -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
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
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
|
-
|
|
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
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
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
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
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
|
-
|
|
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
|
}
|
package/dist/host-bridge.d.ts
CHANGED
|
@@ -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).
|
package/dist/host-bridge.js
CHANGED
|
@@ -175,7 +175,12 @@ async function requestIdentity() {
|
|
|
175
175
|
scheduleRetryOrTimeout();
|
|
176
176
|
return;
|
|
177
177
|
}
|
|
178
|
-
const result = {
|
|
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';
|
package/dist/manifest.d.ts
CHANGED
|
@@ -20,7 +20,10 @@ export interface BuildManifestInput {
|
|
|
20
20
|
queries?: Array<{
|
|
21
21
|
id: string;
|
|
22
22
|
label?: string;
|
|
23
|
-
sql
|
|
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 —
|
package/dist/resolve-identity.js
CHANGED
|
@@ -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() {
|
|
31
|
-
|
|
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:
|
|
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[];
|