@agentorg/cli 0.0.3 → 0.0.5
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 +15 -11
- package/dist/bundled/index.json +4 -4
- package/dist/bundled/{sha256-87e121cbe58279f3a633e61c9773b61fc17fd53eac3d7c6f037925cbfd5d5384.agentorg → sha256-0e91cc3d0e9762b81abc2427b5496b20f383abcd6fb97d928a8d50e956b54d1f.agentorg} +0 -0
- package/dist/bundled/sha256-1f64ca62a0209d767d6aa6c4feb50b0653d60f10bebbc301f79bd3a37e0153c1.agentorg +0 -0
- package/dist/bundled/{sha256-51ecf151e590f5e57f28229d4cb36e4932a2f1670e50ddf1a3ccf335a1ee146d.agentorg → sha256-4055f8fddf6b90f3d356543f554b2a9e9a2c4a8827a8774f8f87952dc1692331.agentorg} +0 -0
- package/dist/bundled/{sha256-b76ec31282e5b863ee9569dfd9d50b283a21101cbe1b3fac23d1cbcf16b9a408.agentorg → sha256-e356ea3c1899138a8494baf521556831006f38534fa9e92ae873e7e6956c3a46.agentorg} +0 -0
- package/{node_modules → dist/node_modules}/@agentorg/hub/dist/src/build.d.ts +2 -2
- package/{node_modules → dist/node_modules}/@agentorg/hub/dist/src/build.js +14 -4
- package/{node_modules → dist/node_modules}/@agentorg/hub/dist/src/hub-actor.js +41 -0
- package/{node_modules → dist/node_modules}/@agentorg/hub/dist/src/index.d.ts +1 -1
- package/{node_modules → dist/node_modules}/@agentorg/hub/dist/src/index.js +1 -1
- package/{node_modules → dist/node_modules}/@agentorg/hub/dist/src/router.js +40 -16
- package/{node_modules → dist/node_modules}/@agentorg/hub/dist/src/state.d.ts +87 -2
- package/{node_modules → dist/node_modules}/@agentorg/hub/dist/src/state.js +312 -60
- package/{node_modules → dist/node_modules}/@agentorg/hub/dist/src/storage.d.ts +2 -1
- package/{node_modules → dist/node_modules}/@agentorg/hub/dist/src/storage.js +3 -2
- package/{node_modules → dist/node_modules}/@agentorg/hub/package.json +3 -0
- package/{node_modules → dist/node_modules}/@agentorg/interfaces/dist/src/hub.d.ts +62 -3
- package/{node_modules → dist/node_modules}/@agentorg/interfaces/dist/src/hub.js +54 -3
- package/{node_modules → dist/node_modules}/@agentorg/interfaces/dist/src/index.d.ts +1 -0
- package/{node_modules → dist/node_modules}/@agentorg/interfaces/dist/src/index.js +1 -0
- package/{node_modules → dist/node_modules}/@agentorg/interfaces/dist/src/installer.d.ts +37 -0
- package/{node_modules → dist/node_modules}/@agentorg/interfaces/dist/src/installer.js +34 -3
- package/dist/node_modules/@agentorg/interfaces/dist/src/rules.d.ts +35 -0
- package/dist/node_modules/@agentorg/interfaces/dist/src/rules.js +37 -0
- package/dist/node_modules/@agentorg/kit/dist/src/chat.d.ts +37 -0
- package/dist/node_modules/@agentorg/kit/dist/src/chat.js +131 -0
- package/dist/node_modules/@agentorg/kit/dist/src/components.d.ts +94 -0
- package/dist/node_modules/@agentorg/kit/dist/src/components.js +66 -0
- package/dist/node_modules/@agentorg/kit/dist/src/icons.d.ts +17 -0
- package/dist/node_modules/@agentorg/kit/dist/src/icons.js +30 -0
- package/dist/node_modules/@agentorg/kit/dist/src/index.d.ts +6 -0
- package/dist/node_modules/@agentorg/kit/dist/src/index.js +10 -0
- package/dist/node_modules/@agentorg/kit/dist/src/markdown.d.ts +9 -0
- package/dist/node_modules/@agentorg/kit/dist/src/markdown.js +14 -0
- package/dist/node_modules/@agentorg/kit/dist/src/resizable.d.ts +5 -0
- package/dist/node_modules/@agentorg/kit/dist/src/resizable.js +8 -0
- package/dist/node_modules/@agentorg/kit/dist/src/styles.d.ts +11 -0
- package/dist/node_modules/@agentorg/kit/dist/src/styles.js +166 -0
- package/dist/node_modules/@agentorg/kit/package.json +44 -0
- package/{node_modules → dist/node_modules}/@agentorg/sdk/dist/frame/runtime.js +172 -17
- package/{node_modules → dist/node_modules}/@agentorg/sdk/dist/host/runner.js +168 -11
- package/{node_modules → dist/node_modules}/@agentorg/sdk/dist/src/client.d.ts +18 -0
- package/{node_modules → dist/node_modules}/@agentorg/sdk/dist/src/client.js +24 -3
- package/dist/node_modules/@agentorg/sdk/dist/src/frame-base.d.ts +8 -0
- package/dist/node_modules/@agentorg/sdk/dist/src/frame-base.js +64 -0
- package/{node_modules → dist/node_modules}/@agentorg/sdk/dist/src/frame-entry.js +4 -4
- package/{node_modules → dist/node_modules}/@agentorg/sdk/dist/src/frame-host.d.ts +1 -10
- package/{node_modules → dist/node_modules}/@agentorg/sdk/dist/src/frame-host.js +0 -23
- package/{node_modules → dist/node_modules}/@agentorg/sdk/dist/src/frame-protocol.d.ts +0 -8
- package/{node_modules → dist/node_modules}/@agentorg/sdk/dist/src/frame.d.ts +0 -2
- package/{node_modules → dist/node_modules}/@agentorg/sdk/dist/src/frame.js +0 -8
- package/{node_modules → dist/node_modules}/@agentorg/sdk/dist/src/host.js +5 -0
- package/{node_modules → dist/node_modules}/@agentorg/sdk/dist/src/index.d.ts +1 -1
- package/{node_modules → dist/node_modules}/@agentorg/sdk/dist/src/module-host.d.ts +6 -1
- package/{node_modules → dist/node_modules}/@agentorg/sdk/dist/src/module-host.js +24 -1
- package/{node_modules → dist/node_modules}/@agentorg/sdk/dist/src/module.d.ts +30 -1
- package/{node_modules → dist/node_modules}/@agentorg/sdk/dist/src/package.js +111 -6
- package/{node_modules → dist/node_modules}/@agentorg/sdk/dist/src/runtime.d.ts +22 -1
- package/{node_modules → dist/node_modules}/@agentorg/sdk/dist/src/runtime.js +114 -0
- package/{node_modules → dist/node_modules}/@agentorg/sdk/dist/src/testing.d.ts +14 -2
- package/dist/node_modules/@agentorg/sdk/dist/src/testing.js +89 -0
- package/{node_modules → dist/node_modules}/@agentorg/shell/dist/app.css +27 -144
- package/dist/node_modules/@agentorg/shell/dist/app.js +219 -0
- package/{node_modules → dist/node_modules}/@agentorg/shell/package.json +1 -0
- package/{node_modules → dist/node_modules}/@agentorg/wire/dist/src/index.d.ts +1 -1
- package/{node_modules → dist/node_modules}/@agentorg/wire/dist/src/ops.d.ts +14 -1
- package/{node_modules → dist/node_modules}/@agentorg/wire/dist/src/ops.js +22 -2
- package/dist/src/cli.js +23 -0
- package/dist/src/gateway.js +42 -1
- package/dist/src/model.d.ts +7 -4
- package/dist/src/model.js +83 -14
- package/dist/src/rules.d.ts +71 -0
- package/dist/src/rules.js +157 -0
- package/package.json +13 -16
- package/dist/bundled/sha256-e4c0e857cea3a721998effe2ca859c4011978b9bc36e63c058bfc17c27a1772e.agentorg +0 -0
- package/node_modules/@agentorg/sdk/dist/src/testing.js +0 -51
- package/node_modules/@agentorg/shell/dist/app.js +0 -9
- /package/{node_modules → dist/node_modules}/@agentorg/hub/dist/src/hub-actor.d.ts +0 -0
- /package/{node_modules → dist/node_modules}/@agentorg/hub/dist/src/logs.d.ts +0 -0
- /package/{node_modules → dist/node_modules}/@agentorg/hub/dist/src/logs.js +0 -0
- /package/{node_modules → dist/node_modules}/@agentorg/hub/dist/src/net.d.ts +0 -0
- /package/{node_modules → dist/node_modules}/@agentorg/hub/dist/src/net.js +0 -0
- /package/{node_modules → dist/node_modules}/@agentorg/hub/dist/src/processes.d.ts +0 -0
- /package/{node_modules → dist/node_modules}/@agentorg/hub/dist/src/processes.js +0 -0
- /package/{node_modules → dist/node_modules}/@agentorg/hub/dist/src/router.d.ts +0 -0
- /package/{node_modules → dist/node_modules}/@agentorg/hub/dist/src/server.d.ts +0 -0
- /package/{node_modules → dist/node_modules}/@agentorg/hub/dist/src/server.js +0 -0
- /package/{node_modules → dist/node_modules}/@agentorg/hub/dist/src/validate-worker.d.ts +0 -0
- /package/{node_modules → dist/node_modules}/@agentorg/hub/dist/src/validate-worker.js +0 -0
- /package/{node_modules → dist/node_modules}/@agentorg/hub/dist/src/validate.d.ts +0 -0
- /package/{node_modules → dist/node_modules}/@agentorg/hub/dist/src/validate.js +0 -0
- /package/{node_modules → dist/node_modules}/@agentorg/interfaces/dist/src/account-login.d.ts +0 -0
- /package/{node_modules → dist/node_modules}/@agentorg/interfaces/dist/src/account-login.js +0 -0
- /package/{node_modules → dist/node_modules}/@agentorg/interfaces/dist/src/chat.d.ts +0 -0
- /package/{node_modules → dist/node_modules}/@agentorg/interfaces/dist/src/chat.js +0 -0
- /package/{node_modules → dist/node_modules}/@agentorg/interfaces/dist/src/http-egress.d.ts +0 -0
- /package/{node_modules → dist/node_modules}/@agentorg/interfaces/dist/src/http-egress.js +0 -0
- /package/{node_modules → dist/node_modules}/@agentorg/interfaces/dist/src/kv.d.ts +0 -0
- /package/{node_modules → dist/node_modules}/@agentorg/interfaces/dist/src/kv.js +0 -0
- /package/{node_modules → dist/node_modules}/@agentorg/interfaces/dist/src/layout.d.ts +0 -0
- /package/{node_modules → dist/node_modules}/@agentorg/interfaces/dist/src/layout.js +0 -0
- /package/{node_modules → dist/node_modules}/@agentorg/interfaces/dist/src/llm-session.d.ts +0 -0
- /package/{node_modules → dist/node_modules}/@agentorg/interfaces/dist/src/llm-session.js +0 -0
- /package/{node_modules → dist/node_modules}/@agentorg/interfaces/dist/src/storage-admin.d.ts +0 -0
- /package/{node_modules → dist/node_modules}/@agentorg/interfaces/dist/src/storage-admin.js +0 -0
- /package/{node_modules → dist/node_modules}/@agentorg/interfaces/dist/src/ui-shell.d.ts +0 -0
- /package/{node_modules → dist/node_modules}/@agentorg/interfaces/dist/src/ui-shell.js +0 -0
- /package/{node_modules → dist/node_modules}/@agentorg/interfaces/dist/src/ui-view.d.ts +0 -0
- /package/{node_modules → dist/node_modules}/@agentorg/interfaces/dist/src/ui-view.js +0 -0
- /package/{node_modules → dist/node_modules}/@agentorg/interfaces/package.json +0 -0
- /package/{node_modules → dist/node_modules}/@agentorg/sdk/dist/src/build.d.ts +0 -0
- /package/{node_modules → dist/node_modules}/@agentorg/sdk/dist/src/build.js +0 -0
- /package/{node_modules → dist/node_modules}/@agentorg/sdk/dist/src/canonical.d.ts +0 -0
- /package/{node_modules → dist/node_modules}/@agentorg/sdk/dist/src/canonical.js +0 -0
- /package/{node_modules → dist/node_modules}/@agentorg/sdk/dist/src/cli.d.ts +0 -0
- /package/{node_modules → dist/node_modules}/@agentorg/sdk/dist/src/cli.js +0 -0
- /package/{node_modules → dist/node_modules}/@agentorg/sdk/dist/src/client-http.d.ts +0 -0
- /package/{node_modules → dist/node_modules}/@agentorg/sdk/dist/src/client-http.js +0 -0
- /package/{node_modules → dist/node_modules}/@agentorg/sdk/dist/src/describe.d.ts +0 -0
- /package/{node_modules → dist/node_modules}/@agentorg/sdk/dist/src/describe.js +0 -0
- /package/{node_modules → dist/node_modules}/@agentorg/sdk/dist/src/errors.d.ts +0 -0
- /package/{node_modules → dist/node_modules}/@agentorg/sdk/dist/src/errors.js +0 -0
- /package/{node_modules → dist/node_modules}/@agentorg/sdk/dist/src/frame-entry.d.ts +0 -0
- /package/{node_modules → dist/node_modules}/@agentorg/sdk/dist/src/frame-protocol.js +0 -0
- /package/{node_modules → dist/node_modules}/@agentorg/sdk/dist/src/host-main.d.ts +0 -0
- /package/{node_modules → dist/node_modules}/@agentorg/sdk/dist/src/host-main.js +0 -0
- /package/{node_modules → dist/node_modules}/@agentorg/sdk/dist/src/host.d.ts +0 -0
- /package/{node_modules → dist/node_modules}/@agentorg/sdk/dist/src/index.js +0 -0
- /package/{node_modules → dist/node_modules}/@agentorg/sdk/dist/src/interface.d.ts +0 -0
- /package/{node_modules → dist/node_modules}/@agentorg/sdk/dist/src/interface.js +0 -0
- /package/{node_modules → dist/node_modules}/@agentorg/sdk/dist/src/manifest-api.d.ts +0 -0
- /package/{node_modules → dist/node_modules}/@agentorg/sdk/dist/src/manifest-api.js +0 -0
- /package/{node_modules → dist/node_modules}/@agentorg/sdk/dist/src/manifest.d.ts +0 -0
- /package/{node_modules → dist/node_modules}/@agentorg/sdk/dist/src/manifest.js +0 -0
- /package/{node_modules → dist/node_modules}/@agentorg/sdk/dist/src/module.js +0 -0
- /package/{node_modules → dist/node_modules}/@agentorg/sdk/dist/src/package.d.ts +0 -0
- /package/{node_modules → dist/node_modules}/@agentorg/sdk/dist/src/person-commands.d.ts +0 -0
- /package/{node_modules → dist/node_modules}/@agentorg/sdk/dist/src/person-commands.js +0 -0
- /package/{node_modules → dist/node_modules}/@agentorg/sdk/dist/src/tar.d.ts +0 -0
- /package/{node_modules → dist/node_modules}/@agentorg/sdk/dist/src/tar.js +0 -0
- /package/{node_modules → dist/node_modules}/@agentorg/sdk/package.json +0 -0
- /package/{node_modules → dist/node_modules}/@agentorg/shell/dist/agent-bindings.js +0 -0
- /package/{node_modules → dist/node_modules}/@agentorg/shell/dist/index.html +0 -0
- /package/{node_modules → dist/node_modules}/@agentorg/shell/src/agent-bindings.ts +0 -0
- /package/{node_modules → dist/node_modules}/@agentorg/wire/dist/src/bytes.d.ts +0 -0
- /package/{node_modules → dist/node_modules}/@agentorg/wire/dist/src/bytes.js +0 -0
- /package/{node_modules → dist/node_modules}/@agentorg/wire/dist/src/frames.d.ts +0 -0
- /package/{node_modules → dist/node_modules}/@agentorg/wire/dist/src/frames.js +0 -0
- /package/{node_modules → dist/node_modules}/@agentorg/wire/dist/src/index.js +0 -0
- /package/{node_modules → dist/node_modules}/@agentorg/wire/dist/src/transports.d.ts +0 -0
- /package/{node_modules → dist/node_modules}/@agentorg/wire/dist/src/transports.js +0 -0
- /package/{node_modules → dist/node_modules}/@agentorg/wire/dist/src/yamux.d.ts +0 -0
- /package/{node_modules → dist/node_modules}/@agentorg/wire/dist/src/yamux.js +0 -0
- /package/{node_modules → dist/node_modules}/@agentorg/wire/package.json +0 -0
|
@@ -23,6 +23,27 @@ const HUB_EXPORTS = {
|
|
|
23
23
|
audit: hubAudit,
|
|
24
24
|
};
|
|
25
25
|
const NAME = /^[a-z][A-Za-z0-9_]*$/;
|
|
26
|
+
/** The longest name of a slot made at run time, so a tool named `<slot>__<method>` fits. */
|
|
27
|
+
const EXPOSED_MAX = 48;
|
|
28
|
+
/** Exposure requests one Actor may have open toward one other at a time. */
|
|
29
|
+
const MAX_OPEN_EXPOSURES = 32;
|
|
30
|
+
/** The Actors that are the Hub itself: they take no slots. */
|
|
31
|
+
const BUILTIN = new Set(["hub", "storage", "net"]);
|
|
32
|
+
/**
|
|
33
|
+
* The default name of a slot exposing `exportName` of the Actor named `providerName`: the name's
|
|
34
|
+
* ASCII words camelCased, `_`, and the export's last part; the export's part alone when the name
|
|
35
|
+
* has no ASCII letters.
|
|
36
|
+
*/
|
|
37
|
+
export function exposedName(providerName, exportName) {
|
|
38
|
+
const words = (providerName.match(/[A-Za-z0-9]+/g) ?? []).map((word) => word.toLowerCase());
|
|
39
|
+
const joined = words
|
|
40
|
+
.map((word, i) => (i ? word[0]?.toUpperCase() + word.slice(1) : word))
|
|
41
|
+
.join("");
|
|
42
|
+
const letters = joined.replace(/^[^A-Za-z]+/, "");
|
|
43
|
+
const camel = letters && (letters[0] ?? "").toLowerCase() + letters.slice(1);
|
|
44
|
+
const part = exportName.slice(exportName.lastIndexOf(".") + 1);
|
|
45
|
+
return (camel ? `${camel}_${part}` : part).slice(0, EXPOSED_MAX);
|
|
46
|
+
}
|
|
26
47
|
const MEMBER = /^[a-z][A-Za-z0-9_]*(\.[a-z][A-Za-z0-9_]*)?$/;
|
|
27
48
|
const now = () => new Date().toISOString();
|
|
28
49
|
const digest = (secret) => createHash("sha256").update(secret).digest();
|
|
@@ -52,6 +73,9 @@ const MIGRATIONS = [
|
|
|
52
73
|
CREATE TABLE audit (
|
|
53
74
|
seq INTEGER PRIMARY KEY AUTOINCREMENT, at TEXT NOT NULL, actor TEXT NOT NULL,
|
|
54
75
|
kind TEXT NOT NULL, details TEXT NOT NULL);`,
|
|
76
|
+
// Exposure: an Actor made for another (provenance only), and requests to expose.
|
|
77
|
+
`ALTER TABLE actors ADD COLUMN installed_for TEXT;
|
|
78
|
+
ALTER TABLE requests ADD COLUMN exposed INTEGER NOT NULL DEFAULT 0;`,
|
|
55
79
|
];
|
|
56
80
|
/** Whether a process with this pid runs now. */
|
|
57
81
|
function alive(pid) {
|
|
@@ -101,7 +125,11 @@ const bindingOf = (row) => ({
|
|
|
101
125
|
interface: String(row.interface),
|
|
102
126
|
madeBy: String(row.made_by),
|
|
103
127
|
at: String(row.at),
|
|
128
|
+
...(row.exposed ? { exposed: true } : {}),
|
|
104
129
|
});
|
|
130
|
+
/** A binding's columns, with whether its slot was made at run time. */
|
|
131
|
+
const BINDING = `b.*, (SELECT d.dynamic FROM declarations d
|
|
132
|
+
WHERE d.actor = b.consumer AND d.side = 'slot' AND d.name = b.slot) AS exposed`;
|
|
105
133
|
const requestOf = (row) => ({
|
|
106
134
|
requestId: String(row.id),
|
|
107
135
|
consumer: String(row.consumer),
|
|
@@ -112,8 +140,21 @@ const requestOf = (row) => ({
|
|
|
112
140
|
requestedBy: String(row.requested_by),
|
|
113
141
|
at: String(row.at),
|
|
114
142
|
needs: row.needs === "slot" ? "slot" : "export",
|
|
143
|
+
...(row.exposed ? { exposed: true } : {}),
|
|
115
144
|
});
|
|
116
145
|
const other = (side) => (side === "export" ? "slot" : "export");
|
|
146
|
+
/** What a package's Actor declares: each module's slots and exports, by `module.name`. */
|
|
147
|
+
function packageDeclarations(manifest) {
|
|
148
|
+
return Object.entries(manifest.modules).flatMap(([module, m]) => [
|
|
149
|
+
["slot", m.slots],
|
|
150
|
+
["export", m.exports],
|
|
151
|
+
].flatMap(([side, members]) => Object.entries(members).map(([member, id]) => {
|
|
152
|
+
const entry = manifest.interfaces[id];
|
|
153
|
+
if (!entry)
|
|
154
|
+
throw new Refused("invalid", `The package doesn't record ${id}.`);
|
|
155
|
+
return { side, name: `${module}.${member}`, iface: { id, ...entry } };
|
|
156
|
+
})));
|
|
157
|
+
}
|
|
117
158
|
/**
|
|
118
159
|
* The core's state in its home folder: Actors, packages, declarations, bindings, binding requests
|
|
119
160
|
* and the audit trail, in `node:sqlite`. Every method that changes something takes its caller, and
|
|
@@ -302,7 +343,7 @@ export class HubState {
|
|
|
302
343
|
// ── Actors ──────────────────────────────────────────────────────────────────
|
|
303
344
|
#actor(actorId) {
|
|
304
345
|
const row = this.#db
|
|
305
|
-
.prepare("SELECT id, name, package, state, restarts, last_exit FROM actors WHERE id = ?")
|
|
346
|
+
.prepare("SELECT id, name, package, state, restarts, last_exit, installed_for FROM actors WHERE id = ?")
|
|
306
347
|
.get(actorId);
|
|
307
348
|
return (row && {
|
|
308
349
|
actorId: String(row.id),
|
|
@@ -311,12 +352,18 @@ export class HubState {
|
|
|
311
352
|
state: row.state,
|
|
312
353
|
restarts: Number(row.restarts),
|
|
313
354
|
...(row.last_exit ? { lastExit: JSON.parse(String(row.last_exit)) } : {}),
|
|
355
|
+
...(row.installed_for ? { installedFor: String(row.installed_for) } : {}),
|
|
314
356
|
});
|
|
315
357
|
}
|
|
358
|
+
/** An Actor as a refusal names it: "Name (id)", or the id alone when there's no such Actor. */
|
|
359
|
+
named(actorId) {
|
|
360
|
+
const actor = this.#actor(actorId);
|
|
361
|
+
return actor ? `${actor.name} (${actor.actorId})` : actorId;
|
|
362
|
+
}
|
|
316
363
|
actor(actorId) {
|
|
317
364
|
const actor = this.#actor(actorId);
|
|
318
365
|
if (!actor)
|
|
319
|
-
throw new Refused("unknown", `No Actor ${actorId}.`);
|
|
366
|
+
throw new Refused("unknown", `No Actor ${actorId}; hub.actors.list lists them.`);
|
|
320
367
|
return actor;
|
|
321
368
|
}
|
|
322
369
|
/** The host sandbox's status changed: every package Actor's record (its `host`) differs. */
|
|
@@ -337,51 +384,49 @@ export class HubState {
|
|
|
337
384
|
}
|
|
338
385
|
#require(caller, hubExport) {
|
|
339
386
|
if (!this.holds(caller, hubExport))
|
|
340
|
-
throw new Refused("forbidden", `${caller} isn't bound to hub.${hubExport}@1.`);
|
|
387
|
+
throw new Refused("forbidden", `${this.named(caller)} isn't bound to hub.${hubExport}@1; a manager (the person) can bind it, or do this for it.`);
|
|
341
388
|
}
|
|
342
389
|
/** Create an Actor: one that runs a package, or an external one, whose credential comes back once. */
|
|
343
390
|
createActor(caller, input) {
|
|
344
391
|
this.#require(caller, "actors");
|
|
392
|
+
if (input.for !== undefined) {
|
|
393
|
+
if (!this.isManager(caller))
|
|
394
|
+
throw new Refused("forbidden", `Only a manager says whom an Actor is made for; ${this.named(caller)} isn't one. Create it without for.`);
|
|
395
|
+
this.#open(input.for);
|
|
396
|
+
}
|
|
345
397
|
const actorId = `act_${randomBytes(8).toString("hex")}`;
|
|
346
398
|
const credential = input.package ? undefined : randomBytes(32).toString("base64url");
|
|
347
|
-
const insert = this.#db.prepare("INSERT INTO actors (id, name, package, state, credential, created_at) VALUES (?, ?, ?, 'created', ?, ?)");
|
|
399
|
+
const insert = this.#db.prepare("INSERT INTO actors (id, name, package, state, credential, created_at, installed_for) VALUES (?, ?, ?, 'created', ?, ?, ?)");
|
|
400
|
+
const madeFor = input.for ?? null;
|
|
348
401
|
this.#transaction(() => {
|
|
349
402
|
if (input.package) {
|
|
350
403
|
const { manifest } = this.package(input.package);
|
|
351
|
-
insert.run(actorId, input.name, input.package, null, now());
|
|
352
|
-
for (const
|
|
353
|
-
|
|
354
|
-
["slot", m.slots],
|
|
355
|
-
["export", m.exports],
|
|
356
|
-
])
|
|
357
|
-
for (const [member, id] of Object.entries(members)) {
|
|
358
|
-
const entry = manifest.interfaces[id];
|
|
359
|
-
if (!entry)
|
|
360
|
-
throw new Refused("invalid", `The package doesn't record ${id}.`);
|
|
361
|
-
this.#declare(actorId, side, `${module}.${member}`, { id, ...entry }, false);
|
|
362
|
-
}
|
|
404
|
+
insert.run(actorId, input.name, input.package, null, now(), madeFor);
|
|
405
|
+
for (const { side, name, iface } of packageDeclarations(manifest))
|
|
406
|
+
this.#declare(actorId, side, name, iface, false);
|
|
363
407
|
// A package's own wiring is applied as it is installed.
|
|
364
408
|
for (const [slot, exported] of manifest.connections)
|
|
365
409
|
this.#insertBinding({ consumer: actorId, slot, provider: actorId, export: exported }, HUB);
|
|
366
410
|
}
|
|
367
411
|
else
|
|
368
|
-
insert.run(actorId, input.name, "external", digest(credential), now());
|
|
412
|
+
insert.run(actorId, input.name, "external", digest(credential), now(), madeFor);
|
|
369
413
|
this.#audit(caller, "actor.create", {
|
|
370
414
|
actorId,
|
|
371
415
|
name: input.name,
|
|
372
416
|
package: input.package ?? "external",
|
|
417
|
+
...(input.for === undefined ? {} : { for: input.for }),
|
|
373
418
|
});
|
|
374
419
|
this.#emit(() => this.#events.actorChanged?.(actorId));
|
|
375
420
|
for (const initial of input.bindings ?? []) {
|
|
376
421
|
const wiring = { consumer: actorId, ...initial };
|
|
377
422
|
if (!this.sides(caller, wiring).export)
|
|
378
|
-
throw new Refused("forbidden", `${caller} can't grant ${initial.provider}'s ${initial.export}.`);
|
|
423
|
+
throw new Refused("forbidden", `${this.named(caller)} can't grant ${this.named(initial.provider)}'s ${initial.export}. Create it without that binding, then it can ask for it with hub.grants.request, or the provider or a manager can bind it.`);
|
|
379
424
|
if (!input.package && !this.declaration(actorId, "slot", initial.slot)) {
|
|
380
425
|
if (!NAME.test(initial.slot))
|
|
381
426
|
throw new Refused("invalid", `${JSON.stringify(initial.slot)} isn't a valid slot name.`);
|
|
382
427
|
const offered = this.declaration(initial.provider, "export", initial.export);
|
|
383
428
|
if (!offered)
|
|
384
|
-
throw new Refused("unknown", `${initial.provider} offers no ${initial.export}.`);
|
|
429
|
+
throw new Refused("unknown", `${this.named(initial.provider)} offers no ${initial.export}; hub.exports.list lists what it offers.`);
|
|
385
430
|
this.#declare(actorId, "slot", initial.slot, offered.iface, false);
|
|
386
431
|
}
|
|
387
432
|
this.#insertBinding(wiring, caller);
|
|
@@ -403,14 +448,14 @@ export class HubState {
|
|
|
403
448
|
this.#require(caller, "actors");
|
|
404
449
|
const actor = this.actor(actorId);
|
|
405
450
|
if (actor.package === "external" || actor.package === "builtin")
|
|
406
|
-
throw new Refused("state", `${actorId} isn't a package Actor
|
|
451
|
+
throw new Refused("state", `${this.named(actorId)} isn't a package Actor, so the Hub doesn't run its process.`);
|
|
407
452
|
return actor;
|
|
408
453
|
}
|
|
409
454
|
/** Refused unless the caller may start the Actor and it can be started now; changes nothing. */
|
|
410
455
|
checkStart(caller, actorId) {
|
|
411
456
|
const actor = this.#packageActor(caller, actorId);
|
|
412
457
|
if (actor.state !== "created" && actor.state !== "stopped")
|
|
413
|
-
throw new Refused("state", `${actorId} is ${actor.state}.`);
|
|
458
|
+
throw new Refused("state", `${this.named(actorId)} is ${actor.state === "running" ? "already running" : "closed and can't start again; make another"}.`);
|
|
414
459
|
}
|
|
415
460
|
/** Start a package Actor: recorded as running; the Hub spawns its process. */
|
|
416
461
|
start(caller, actorId) {
|
|
@@ -424,7 +469,7 @@ export class HubState {
|
|
|
424
469
|
stop(caller, actorId) {
|
|
425
470
|
const actor = this.#packageActor(caller, actorId);
|
|
426
471
|
if (actor.state !== "running")
|
|
427
|
-
throw new Refused("state", `${actorId} is ${actor.state}.`);
|
|
472
|
+
throw new Refused("state", `${this.named(actorId)} is ${actor.state}, not running.`);
|
|
428
473
|
this.#transaction(() => {
|
|
429
474
|
this.#db.prepare("UPDATE actors SET state = 'stopped' WHERE id = ?").run(actorId);
|
|
430
475
|
this.#audit(caller, "actor.stop", { actorId });
|
|
@@ -436,7 +481,7 @@ export class HubState {
|
|
|
436
481
|
this.#require(caller, "actors");
|
|
437
482
|
const actor = this.actor(actorId);
|
|
438
483
|
if (actorId === HUB || actorId === PERSON || actorId === STORAGE || actorId === NET)
|
|
439
|
-
throw new Refused("forbidden", `${actorId} can't be closed.`);
|
|
484
|
+
throw new Refused("forbidden", `${this.named(actorId)} is built in and can't be closed.`);
|
|
440
485
|
if (actor.state === "closed")
|
|
441
486
|
return;
|
|
442
487
|
this.#transaction(() => {
|
|
@@ -452,6 +497,112 @@ export class HubState {
|
|
|
452
497
|
this.#emit(() => this.#events.closed?.(actorId));
|
|
453
498
|
});
|
|
454
499
|
}
|
|
500
|
+
/**
|
|
501
|
+
* Drop the bindings through one of an Actor's slots or exports, its interface having changed,
|
|
502
|
+
* but those `keeps` says still match: each is audited as `binding.drop`. Dropping only takes
|
|
503
|
+
* access away. Returns what was dropped.
|
|
504
|
+
*/
|
|
505
|
+
#dropBindingsOf(actorId, side, name, by, keeps = () => false) {
|
|
506
|
+
const dropped = this.bindings(actorId).filter((b) => (side === "slot"
|
|
507
|
+
? b.consumer === actorId && b.slot === name
|
|
508
|
+
: b.provider === actorId && b.export === name) && !keeps(b));
|
|
509
|
+
for (const b of dropped)
|
|
510
|
+
this.#removeBindings("consumer = ? AND slot = ?", b.consumer, b.slot);
|
|
511
|
+
const wirings = dropped.map(({ consumer, slot, provider, export: exported }) => ({
|
|
512
|
+
consumer,
|
|
513
|
+
slot,
|
|
514
|
+
provider,
|
|
515
|
+
export: exported,
|
|
516
|
+
}));
|
|
517
|
+
for (const wiring of wirings)
|
|
518
|
+
this.#audit(by, "binding.drop", { ...wiring, why: "its interface changed" });
|
|
519
|
+
return wirings;
|
|
520
|
+
}
|
|
521
|
+
/** Close the open requests to bind through one of an Actor's slots or exports. */
|
|
522
|
+
#closeRequestsThrough(actorId, side, name) {
|
|
523
|
+
const where = side === "slot" ? "consumer = ? AND slot = ?" : "provider = ? AND export = ?";
|
|
524
|
+
for (const open of this.#pending(where, actorId, name))
|
|
525
|
+
this.#decide(open, "closed");
|
|
526
|
+
}
|
|
527
|
+
/** Refused unless the caller may upgrade the Actor to the package now; changes nothing. */
|
|
528
|
+
checkUpgrade(caller, actorId, hash) {
|
|
529
|
+
this.#require(caller, "actors");
|
|
530
|
+
// An upgrade runs new code as an Actor that keeps its bindings and its store: whatever it was
|
|
531
|
+
// granted, the new code holds. Only a manager, who could grant all of it anyway, may.
|
|
532
|
+
if (!this.isManager(caller))
|
|
533
|
+
throw new Refused("forbidden", `Only a manager may upgrade an Actor, since the new package keeps its bindings and its store; ${this.named(caller)} isn't one.`);
|
|
534
|
+
const actor = this.actor(actorId);
|
|
535
|
+
if (actorId === HUB || actorId === PERSON || actorId === STORAGE || actorId === NET)
|
|
536
|
+
throw new Refused("forbidden", `${this.named(actorId)} is built in and can't be upgraded.`);
|
|
537
|
+
if (actor.package === "external")
|
|
538
|
+
throw new Refused("state", `${this.named(actorId)} isn't a package Actor; it declares what it takes when it connects.`);
|
|
539
|
+
if (actor.state === "closed")
|
|
540
|
+
throw new Refused("state", `${this.named(actorId)} is closed for good; make another.`);
|
|
541
|
+
packageDeclarations(this.package(hash).manifest);
|
|
542
|
+
return actor;
|
|
543
|
+
}
|
|
544
|
+
/**
|
|
545
|
+
* Run another package as the same Actor: its id, its stores and every binding whose interfaces
|
|
546
|
+
* still match stay; a binding through a slot or export the new package drops or declares as
|
|
547
|
+
* another interface is dropped, audited and returned. The new package's own wiring is applied
|
|
548
|
+
* where its slot is unbound. The process isn't touched here: a running Actor is stopped first
|
|
549
|
+
* and started after, by whoever runs processes.
|
|
550
|
+
*/
|
|
551
|
+
upgrade(caller, actorId, hash) {
|
|
552
|
+
const actor = this.checkUpgrade(caller, actorId, hash);
|
|
553
|
+
if (actor.package === hash)
|
|
554
|
+
return [];
|
|
555
|
+
const { manifest } = this.package(hash);
|
|
556
|
+
const next = packageDeclarations(manifest);
|
|
557
|
+
const declared = (side, name) => next.find((n) => n.side === side && n.name === name)?.iface;
|
|
558
|
+
const dropped = [];
|
|
559
|
+
this.#transaction(() => {
|
|
560
|
+
// What it offered at run time was its process's, which this restarts: it goes, with the
|
|
561
|
+
// bindings to it, which are returned with the rest.
|
|
562
|
+
for (const row of this.#db
|
|
563
|
+
.prepare("SELECT name FROM declarations WHERE actor = ? AND side = 'export' AND dynamic = 1")
|
|
564
|
+
.all(actorId)) {
|
|
565
|
+
dropped.push(...this.#dropBindingsOf(actorId, "export", String(row.name), caller));
|
|
566
|
+
this.#drop(actorId, "export", String(row.name));
|
|
567
|
+
}
|
|
568
|
+
// A binding stays while its two ends declare the same interface: this Actor's end as the
|
|
569
|
+
// new package declares it, the other end as it is declared now.
|
|
570
|
+
const keeps = (b) => {
|
|
571
|
+
const slot = b.consumer === actorId
|
|
572
|
+
? declared("slot", b.slot)
|
|
573
|
+
: this.declaration(b.consumer, "slot", b.slot)?.iface;
|
|
574
|
+
const exported = b.provider === actorId
|
|
575
|
+
? declared("export", b.export)
|
|
576
|
+
: this.declaration(b.provider, "export", b.export)?.iface;
|
|
577
|
+
return !!slot && !!exported && compareInterfaces(slot, exported) === "same";
|
|
578
|
+
};
|
|
579
|
+
const old = this.#db
|
|
580
|
+
.prepare("SELECT side, name, iface FROM declarations WHERE actor = ? AND dynamic = 0")
|
|
581
|
+
.all(actorId).map((row) => ({
|
|
582
|
+
side: row.side,
|
|
583
|
+
name: String(row.name),
|
|
584
|
+
iface: JSON.parse(String(row.iface)),
|
|
585
|
+
}));
|
|
586
|
+
for (const was of old) {
|
|
587
|
+
dropped.push(...this.#dropBindingsOf(actorId, was.side, was.name, caller, keeps));
|
|
588
|
+
// A request through a name the package no longer declares as it was can't be made.
|
|
589
|
+
const fresh = declared(was.side, was.name);
|
|
590
|
+
if (!fresh || compareInterfaces(was.iface, fresh) !== "same")
|
|
591
|
+
this.#closeRequestsThrough(actorId, was.side, was.name);
|
|
592
|
+
}
|
|
593
|
+
this.#db.prepare("DELETE FROM declarations WHERE actor = ? AND dynamic = 0").run(actorId);
|
|
594
|
+
for (const { side, name, iface } of next)
|
|
595
|
+
this.#declare(actorId, side, name, iface, false);
|
|
596
|
+
this.#db.prepare("UPDATE actors SET package = ? WHERE id = ?").run(hash, actorId);
|
|
597
|
+
for (const [slot, exported] of manifest.connections)
|
|
598
|
+
if (!this.binding(actorId, slot))
|
|
599
|
+
this.#insertBinding({ consumer: actorId, slot, provider: actorId, export: exported }, HUB);
|
|
600
|
+
this.#audit(caller, "actor.upgrade", { actorId, from: actor.package, to: hash, dropped });
|
|
601
|
+
this.#emit(() => this.#events.exportsChanged?.(actorId));
|
|
602
|
+
this.#emit(() => this.#events.actorChanged?.(actorId));
|
|
603
|
+
});
|
|
604
|
+
return dropped;
|
|
605
|
+
}
|
|
455
606
|
// ── Packages ────────────────────────────────────────────────────────────────
|
|
456
607
|
/** Add a `.agentorg` archive from a path on this machine, verified; adding it again is a no-op. */
|
|
457
608
|
addPackage(caller, path) {
|
|
@@ -557,18 +708,19 @@ export class HubState {
|
|
|
557
708
|
#open(actorId) {
|
|
558
709
|
const actor = this.actor(actorId);
|
|
559
710
|
if (actor.state === "closed")
|
|
560
|
-
throw new Refused("state", `${actorId} is closed.`);
|
|
711
|
+
throw new Refused("state", `${this.named(actorId)} is closed for good; make another.`);
|
|
561
712
|
return actor;
|
|
562
713
|
}
|
|
563
714
|
/**
|
|
564
715
|
* An external Actor's slots and static exports, as it declares them when it connects; they
|
|
565
|
-
* replace what it declared before. A
|
|
566
|
-
*
|
|
716
|
+
* replace what it declared before. A name redeclared as another interface loses its bindings
|
|
717
|
+
* (audited as `binding.drop`), since a binding holds only while both ends declare the same one.
|
|
718
|
+
* A name it no longer declares is dropped, with its bindings.
|
|
567
719
|
*/
|
|
568
720
|
declareExternal(actorId, declares) {
|
|
569
721
|
const actor = this.#open(actorId);
|
|
570
722
|
if (actor.package !== "external")
|
|
571
|
-
throw new Refused("forbidden", `${actorId} declares through its package.`);
|
|
723
|
+
throw new Refused("forbidden", `${this.named(actorId)} declares through its package; an Actor of another package declares otherwise.`);
|
|
572
724
|
this.#transaction(() => {
|
|
573
725
|
for (const [side, members] of [
|
|
574
726
|
["slot", declares.slots],
|
|
@@ -578,11 +730,27 @@ export class HubState {
|
|
|
578
730
|
if (!NAME.test(name))
|
|
579
731
|
throw new Refused("invalid", `${JSON.stringify(name)} isn't a valid name.`);
|
|
580
732
|
this.#checked(iface);
|
|
733
|
+
if (side === "slot") {
|
|
734
|
+
// The Actor's own slots are its to name: an exposure under the same name yields,
|
|
735
|
+
// with its binding, and requests to make one under it close.
|
|
736
|
+
if (this.declaration(actorId, side, name)?.dynamic)
|
|
737
|
+
this.#drop(actorId, side, name);
|
|
738
|
+
for (const open of this.#pending("consumer = ? AND slot = ? AND exposed = 1", actorId, name))
|
|
739
|
+
this.#decide(open, "closed");
|
|
740
|
+
}
|
|
741
|
+
// A binding holds while both ends declare the same interface: redeclared as another,
|
|
742
|
+
// the name's bindings whose other end doesn't declare it are dropped, and its requests
|
|
743
|
+
// close.
|
|
581
744
|
const known = this.declaration(actorId, side, name);
|
|
582
|
-
if (known &&
|
|
583
|
-
this.#
|
|
584
|
-
|
|
585
|
-
|
|
745
|
+
if (known && compareInterfaces(known.iface, iface) !== "same") {
|
|
746
|
+
this.#dropBindingsOf(actorId, side, name, actorId, (b) => {
|
|
747
|
+
const other = side === "slot"
|
|
748
|
+
? this.declaration(b.provider, "export", b.export)?.iface
|
|
749
|
+
: this.declaration(b.consumer, "slot", b.slot)?.iface;
|
|
750
|
+
return !!other && compareInterfaces(iface, other) === "same";
|
|
751
|
+
});
|
|
752
|
+
this.#closeRequestsThrough(actorId, side, name);
|
|
753
|
+
}
|
|
586
754
|
this.#declare(actorId, side, name, iface, false);
|
|
587
755
|
}
|
|
588
756
|
const kept = Object.keys(members);
|
|
@@ -598,13 +766,6 @@ export class HubState {
|
|
|
598
766
|
this.#emit(() => this.#events.exportsChanged?.(actorId));
|
|
599
767
|
});
|
|
600
768
|
}
|
|
601
|
-
#isBound(actor, side, name) {
|
|
602
|
-
return !!this.#db
|
|
603
|
-
.prepare(side === "slot"
|
|
604
|
-
? "SELECT 1 FROM bindings WHERE consumer = ? AND slot = ?"
|
|
605
|
-
: "SELECT 1 FROM bindings WHERE provider = ? AND export = ?")
|
|
606
|
-
.get(actor, name);
|
|
607
|
-
}
|
|
608
769
|
/** A declaration gone, and the bindings through it. */
|
|
609
770
|
#drop(actor, side, name) {
|
|
610
771
|
this.#db
|
|
@@ -622,7 +783,7 @@ export class HubState {
|
|
|
622
783
|
if (!MEMBER.test(name))
|
|
623
784
|
throw new Refused("invalid", `${JSON.stringify(name)} isn't a valid export name.`);
|
|
624
785
|
if (this.declaration(actorId, "export", name))
|
|
625
|
-
throw new Refused("conflict", `${actorId} already offers ${name}.`);
|
|
786
|
+
throw new Refused("conflict", `${this.named(actorId)} already offers ${name}; offer it under another name${this.declaration(actorId, "export", name)?.dynamic ? ", or withdraw it first" : ""}.`);
|
|
626
787
|
this.#checked(iface);
|
|
627
788
|
this.#transaction(() => {
|
|
628
789
|
this.#declare(actorId, "export", name, iface, true);
|
|
@@ -634,7 +795,7 @@ export class HubState {
|
|
|
634
795
|
withdraw(actorId, name) {
|
|
635
796
|
const declared = this.declaration(actorId, "export", name);
|
|
636
797
|
if (!declared?.dynamic)
|
|
637
|
-
throw new Refused("unknown", `${actorId} offers no ${name} at run time to withdraw.`);
|
|
798
|
+
throw new Refused("unknown", `${this.named(actorId)} offers no ${name} at run time to withdraw; only exports offered at run time can be.`);
|
|
638
799
|
this.#transaction(() => {
|
|
639
800
|
this.#drop(actorId, "export", name);
|
|
640
801
|
this.#audit(actorId, "export.withdraw", { export: name });
|
|
@@ -673,7 +834,7 @@ export class HubState {
|
|
|
673
834
|
// ── Bindings: a binding is the grant ────────────────────────────────────────
|
|
674
835
|
binding(consumer, slot) {
|
|
675
836
|
const row = this.#db
|
|
676
|
-
.prepare(
|
|
837
|
+
.prepare(`SELECT ${BINDING} FROM bindings b WHERE consumer = ? AND slot = ?`)
|
|
677
838
|
.get(consumer, slot);
|
|
678
839
|
return row && bindingOf(row);
|
|
679
840
|
}
|
|
@@ -691,9 +852,9 @@ export class HubState {
|
|
|
691
852
|
bindings(actor) {
|
|
692
853
|
const rows = actor
|
|
693
854
|
? this.#db
|
|
694
|
-
.prepare(
|
|
855
|
+
.prepare(`SELECT ${BINDING} FROM bindings b WHERE consumer = ? OR provider = ? ORDER BY b.rowid`)
|
|
695
856
|
.all(actor, actor)
|
|
696
|
-
: this.#db.prepare(
|
|
857
|
+
: this.#db.prepare(`SELECT ${BINDING} FROM bindings b ORDER BY b.rowid`).all();
|
|
697
858
|
return rows.map(bindingOf);
|
|
698
859
|
}
|
|
699
860
|
/** A manager is bound to the hub's `hub.bindings@1`; there is no other role. */
|
|
@@ -737,10 +898,10 @@ export class HubState {
|
|
|
737
898
|
this.#open(wiring.provider);
|
|
738
899
|
const slot = this.declaration(wiring.consumer, "slot", wiring.slot);
|
|
739
900
|
if (!slot)
|
|
740
|
-
throw new Refused("unknown", `${wiring.consumer} has no slot ${wiring.slot}.`);
|
|
901
|
+
throw new Refused("unknown", `${this.named(wiring.consumer)} has no slot ${wiring.slot}; hub.grants.expose makes one at run time.`);
|
|
741
902
|
const exported = this.declaration(wiring.provider, "export", wiring.export);
|
|
742
903
|
if (!exported)
|
|
743
|
-
throw new Refused("unknown", `${wiring.provider} offers no ${wiring.export}.`);
|
|
904
|
+
throw new Refused("unknown", `${this.named(wiring.provider)} offers no ${wiring.export}; hub.exports.list lists what it offers.`);
|
|
744
905
|
const fit = compareInterfaces(slot.iface, exported.iface);
|
|
745
906
|
if (fit === "unrelated")
|
|
746
907
|
throw new Refused("conflict", `${wiring.slot} takes ${slot.iface.id}; ${wiring.export} offers ${exported.iface.id}.`);
|
|
@@ -751,7 +912,7 @@ export class HubState {
|
|
|
751
912
|
#insertBinding(wiring, madeBy) {
|
|
752
913
|
const iface = this.#ends(wiring);
|
|
753
914
|
if (this.binding(wiring.consumer, wiring.slot))
|
|
754
|
-
throw new Refused("bound", `${wiring.consumer}'s ${wiring.slot} is already bound; unbind it first.`);
|
|
915
|
+
throw new Refused("bound", `${this.named(wiring.consumer)}'s ${wiring.slot} is already bound; unbind it first.`);
|
|
755
916
|
const at = now();
|
|
756
917
|
this.#db
|
|
757
918
|
.prepare("INSERT INTO bindings (consumer, slot, provider, export, interface, made_by, at) VALUES (?, ?, ?, ?, ?, ?, ?)")
|
|
@@ -761,14 +922,19 @@ export class HubState {
|
|
|
761
922
|
this.#emit(() => this.#events.bound?.(record));
|
|
762
923
|
}
|
|
763
924
|
#removeBindings(where, ...params) {
|
|
764
|
-
const removed = this.#db.prepare(`SELECT
|
|
925
|
+
const removed = this.#db.prepare(`SELECT ${BINDING} FROM bindings b WHERE ${where}`).all(...params).map(bindingOf);
|
|
765
926
|
if (!removed.length)
|
|
766
927
|
return;
|
|
767
|
-
if (removed.some((b) => b.consumer === PERSON && b.provider === HUB))
|
|
928
|
+
if (removed.some((b) => b.consumer === PERSON && b.provider === HUB && !b.exposed))
|
|
768
929
|
throw new Refused("forbidden", "The person's hub bindings stay, so someone can always manage the Hub.");
|
|
769
930
|
// Requests whose requester held its end through these bindings can't be approved any more.
|
|
770
931
|
const open = this.#pending("1 = 1");
|
|
771
932
|
this.#db.prepare(`DELETE FROM bindings WHERE ${where}`).run(...params);
|
|
933
|
+
// A slot made at run time goes with its binding.
|
|
934
|
+
const dropExposed = this.#db.prepare("DELETE FROM declarations WHERE actor = ? AND side = 'slot' AND name = ? AND dynamic = 1");
|
|
935
|
+
for (const binding of removed)
|
|
936
|
+
if (binding.exposed)
|
|
937
|
+
dropExposed.run(binding.consumer, binding.slot);
|
|
772
938
|
this.#emit(() => this.#events.unbound?.(removed));
|
|
773
939
|
for (const pending of open)
|
|
774
940
|
if (!this.#standing(pending.request))
|
|
@@ -783,7 +949,7 @@ export class HubState {
|
|
|
783
949
|
bind(caller, wiring) {
|
|
784
950
|
const sides = this.sides(caller, wiring);
|
|
785
951
|
if (!sides.export && !sides.slot)
|
|
786
|
-
throw new Refused("forbidden", `${caller} holds neither end of this binding.`);
|
|
952
|
+
throw new Refused("forbidden", `${this.named(caller)} holds neither end of this binding: the export side is ${this.named(wiring.provider)}, an Actor bound to that export, or a manager; the slot side is ${this.named(wiring.consumer)} or a manager. Ask one of them.`);
|
|
787
953
|
if (!(sides.export && sides.slot)) {
|
|
788
954
|
const has = sides.export ? "export" : "slot";
|
|
789
955
|
if (!this.#asked(wiring, has))
|
|
@@ -805,9 +971,9 @@ export class HubState {
|
|
|
805
971
|
unbind(caller, consumer, slot) {
|
|
806
972
|
const binding = this.binding(consumer, slot);
|
|
807
973
|
if (!binding)
|
|
808
|
-
throw new Refused("unknown", `${consumer}'s ${slot} isn't bound.`);
|
|
974
|
+
throw new Refused("unknown", `${this.named(consumer)}'s ${slot} isn't bound.`);
|
|
809
975
|
if (caller !== consumer && caller !== binding.provider && !this.isManager(caller))
|
|
810
|
-
throw new Refused("forbidden", `${caller} can't unbind ${consumer}'s ${slot}.`);
|
|
976
|
+
throw new Refused("forbidden", `${this.named(caller)} can't unbind ${this.named(consumer)}'s ${slot}: its consumer, its provider or a manager can.`);
|
|
811
977
|
this.#transaction(() => {
|
|
812
978
|
this.#removeBindings("consumer = ? AND slot = ?", consumer, slot);
|
|
813
979
|
this.#audit(caller, "unbind", {
|
|
@@ -818,6 +984,84 @@ export class HubState {
|
|
|
818
984
|
});
|
|
819
985
|
});
|
|
820
986
|
}
|
|
987
|
+
/**
|
|
988
|
+
* Expose an export to one Actor: a slot made on `to` at run time, typed by the export's
|
|
989
|
+
* interface and bound to it, in one step. The authority is a binding's, with `to` as the
|
|
990
|
+
* consumer: with both ends it is made (approving any open request for it); with one, it is a
|
|
991
|
+
* request to the other end's approvers, who approve by exposing the same; with neither, it is
|
|
992
|
+
* refused. The same export already exposed to `to` gives that slot back.
|
|
993
|
+
*/
|
|
994
|
+
expose(caller, input) {
|
|
995
|
+
const to = this.#open(input.to);
|
|
996
|
+
const provider = this.#open(input.provider);
|
|
997
|
+
// Authority first, as bind's: a caller with neither end learns nothing more.
|
|
998
|
+
const ends = { consumer: to.actorId, provider: provider.actorId, export: input.export };
|
|
999
|
+
const sides = this.sides(caller, { ...ends, slot: "" });
|
|
1000
|
+
if (!sides.export && !sides.slot)
|
|
1001
|
+
throw new Refused("forbidden", `${this.named(caller)} can't expose ${input.export} of ${this.named(provider.actorId)} to ${this.named(to.actorId)}: that takes the provider's side (it, an Actor bound to that export, or a manager) or ${this.named(to.actorId)}'s (it, or a manager). Ask one of them to expose it.`);
|
|
1002
|
+
if (BUILTIN.has(to.actorId))
|
|
1003
|
+
throw new Refused("forbidden", `${this.named(to.actorId)} is the Hub itself and takes no slots.`);
|
|
1004
|
+
const offered = this.declaration(provider.actorId, "export", input.export);
|
|
1005
|
+
if (!offered)
|
|
1006
|
+
throw new Refused("unknown", `${this.named(provider.actorId)} offers no ${input.export}.`);
|
|
1007
|
+
if (offered.iface.kind !== "rpc")
|
|
1008
|
+
throw new Refused("unsupported", `Only an rpc export can be exposed; ${input.export} is ${offered.iface.kind}.`);
|
|
1009
|
+
const same = (row) => row.provider === provider.actorId && row.export === input.export;
|
|
1010
|
+
const already = this.bindings(to.actorId).find((b) => b.consumer === to.actorId && b.exposed && same(b));
|
|
1011
|
+
if (already && (input.as === undefined || input.as === already.slot))
|
|
1012
|
+
return { exposed: true, slot: already.slot };
|
|
1013
|
+
const requested = this.#db
|
|
1014
|
+
.prepare("SELECT * FROM requests WHERE status = 'open' AND exposed = 1 AND consumer = ?")
|
|
1015
|
+
.all(to.actorId).map(requestOf);
|
|
1016
|
+
// An open request's name for this export is taken again, so approving needs no `as`.
|
|
1017
|
+
const slot = input.as ??
|
|
1018
|
+
requested.find(same)?.slot ??
|
|
1019
|
+
this.#freeName(to.actorId, exposedName(provider.name, input.export), requested);
|
|
1020
|
+
if (!NAME.test(slot) || slot.length > EXPOSED_MAX)
|
|
1021
|
+
throw new Refused("invalid", `${JSON.stringify(slot)} isn't a slot name: one word of letters, digits and _, at most ${EXPOSED_MAX}.`);
|
|
1022
|
+
// A request reserves no name: the first exposure made under it has it, and the rest close.
|
|
1023
|
+
if (this.declaration(to.actorId, "slot", slot))
|
|
1024
|
+
throw new Refused("conflict", `${this.named(to.actorId)} already has a slot ${slot}; name another with as.`);
|
|
1025
|
+
const wiring = { ...ends, slot };
|
|
1026
|
+
if (!(sides.export && sides.slot)) {
|
|
1027
|
+
const has = sides.export ? "export" : "slot";
|
|
1028
|
+
if (!this.#asked(wiring, has)) {
|
|
1029
|
+
const mine = requested.filter((r) => r.requestedBy === caller && !(same(r) && r.slot === slot));
|
|
1030
|
+
if (mine.length >= MAX_OPEN_EXPOSURES)
|
|
1031
|
+
throw new Refused("limit", `${this.named(caller)} has ${MAX_OPEN_EXPOSURES} exposures to ${this.named(to.actorId)} waiting; wait for them to be decided, or withdraw one by exposing it.`);
|
|
1032
|
+
const request = this.#ask(caller, wiring, "", other(has), true);
|
|
1033
|
+
return { exposed: false, slot, requestId: request.requestId };
|
|
1034
|
+
}
|
|
1035
|
+
}
|
|
1036
|
+
this.#transaction(() => {
|
|
1037
|
+
// Settled: this slot's requests (approved if for this export), and this export's requests
|
|
1038
|
+
// to `to` under other names, which can't be made any more.
|
|
1039
|
+
const pending = this.#pending("consumer = ? AND (slot = ? OR (exposed = 1 AND provider = ? AND export = ?))", wiring.consumer, wiring.slot, wiring.provider, wiring.export);
|
|
1040
|
+
this.#declare(to.actorId, "slot", slot, offered.iface, true);
|
|
1041
|
+
this.#insertBinding(wiring, caller);
|
|
1042
|
+
for (const open of pending)
|
|
1043
|
+
this.#decide(open, same(open.request) && open.request.slot === slot ? "approved" : "closed");
|
|
1044
|
+
this.#audit(caller, "expose", { ...wiring });
|
|
1045
|
+
});
|
|
1046
|
+
return { exposed: true, slot };
|
|
1047
|
+
}
|
|
1048
|
+
/** A name free on `to`: `base`, else `base_2`, `base_3`…, each within the longest name. */
|
|
1049
|
+
#freeName(to, base, requested) {
|
|
1050
|
+
const taken = (name) => !!this.declaration(to, "slot", name) || requested.some((r) => r.slot === name);
|
|
1051
|
+
for (let n = 1;; n++) {
|
|
1052
|
+
const suffix = n === 1 ? "" : `_${n}`;
|
|
1053
|
+
const name = base.slice(0, EXPOSED_MAX - suffix.length) + suffix;
|
|
1054
|
+
if (!taken(name))
|
|
1055
|
+
return name;
|
|
1056
|
+
}
|
|
1057
|
+
}
|
|
1058
|
+
/** Remove a slot made by `expose`, with its binding: its consumer, its provider or a manager may. */
|
|
1059
|
+
unexpose(caller, to, slot) {
|
|
1060
|
+
const binding = this.binding(to, slot);
|
|
1061
|
+
if (!binding?.exposed)
|
|
1062
|
+
throw new Refused("unknown", `${this.named(to)} has no slot ${slot} made by expose; unbind a declared slot instead.`);
|
|
1063
|
+
this.unbind(caller, to, slot);
|
|
1064
|
+
}
|
|
821
1065
|
/**
|
|
822
1066
|
* Ask for a binding the caller holds one end of; it goes to the other end's approvers. Asking
|
|
823
1067
|
* again for the same wiring while a request is open returns that request.
|
|
@@ -825,9 +1069,9 @@ export class HubState {
|
|
|
825
1069
|
request(caller, wiring, reason) {
|
|
826
1070
|
const sides = this.sides(caller, wiring);
|
|
827
1071
|
if (!sides.export && !sides.slot)
|
|
828
|
-
throw new Refused("forbidden", `${caller} holds neither end of this binding.`);
|
|
1072
|
+
throw new Refused("forbidden", `${this.named(caller)} holds neither end of this binding: the export side is ${this.named(wiring.provider)}, an Actor bound to that export, or a manager; the slot side is ${this.named(wiring.consumer)} or a manager. Ask one of them.`);
|
|
829
1073
|
if (sides.export && sides.slot)
|
|
830
|
-
throw new Refused("state", `${caller} holds both ends: bind it instead.`);
|
|
1074
|
+
throw new Refused("state", `${this.named(caller)} holds both ends: bind it instead.`);
|
|
831
1075
|
const has = sides.export ? "export" : "slot";
|
|
832
1076
|
if (this.#asked(wiring, has))
|
|
833
1077
|
throw new Refused("state", "The other end has asked already: bind to approve.");
|
|
@@ -841,10 +1085,12 @@ export class HubState {
|
|
|
841
1085
|
.map(requestOf)
|
|
842
1086
|
.some((request) => this.#standing(request));
|
|
843
1087
|
}
|
|
844
|
-
#ask(caller, wiring, reason, needs) {
|
|
845
|
-
|
|
1088
|
+
#ask(caller, wiring, reason, needs, exposed = false) {
|
|
1089
|
+
// An exposure's slot doesn't exist until it is approved; `expose` checked its ends.
|
|
1090
|
+
if (!exposed)
|
|
1091
|
+
this.#ends(wiring);
|
|
846
1092
|
if (this.binding(wiring.consumer, wiring.slot))
|
|
847
|
-
throw new Refused("bound", `${wiring.consumer}'s ${wiring.slot} is already bound.`);
|
|
1093
|
+
throw new Refused("bound", `${this.named(wiring.consumer)}'s ${wiring.slot} is already bound; unbind it first.`);
|
|
848
1094
|
const open = this.#db
|
|
849
1095
|
.prepare("SELECT * FROM requests WHERE status = 'open' AND requested_by = ? AND consumer = ? AND slot = ? AND provider = ? AND export = ?")
|
|
850
1096
|
.get(caller, wiring.consumer, wiring.slot, wiring.provider, wiring.export);
|
|
@@ -857,12 +1103,18 @@ export class HubState {
|
|
|
857
1103
|
reason,
|
|
858
1104
|
requestedBy: caller,
|
|
859
1105
|
at: now(),
|
|
1106
|
+
...(exposed ? { exposed: true } : {}),
|
|
860
1107
|
};
|
|
861
1108
|
this.#transaction(() => {
|
|
862
1109
|
this.#db
|
|
863
|
-
.prepare("INSERT INTO requests (id, consumer, slot, provider, export, reason, requested_by, at, status, needs) VALUES (?, ?, ?, ?, ?, ?, ?, ?, 'open', ?)")
|
|
864
|
-
.run(request.requestId, wiring.consumer, wiring.slot, wiring.provider, wiring.export, reason, caller, request.at, needs);
|
|
865
|
-
this.#audit(caller, "request", {
|
|
1110
|
+
.prepare("INSERT INTO requests (id, consumer, slot, provider, export, reason, requested_by, at, status, needs, exposed) VALUES (?, ?, ?, ?, ?, ?, ?, ?, 'open', ?, ?)")
|
|
1111
|
+
.run(request.requestId, wiring.consumer, wiring.slot, wiring.provider, wiring.export, reason, caller, request.at, needs, exposed ? 1 : 0);
|
|
1112
|
+
this.#audit(caller, "request", {
|
|
1113
|
+
...wiring,
|
|
1114
|
+
needs,
|
|
1115
|
+
requestId: request.requestId,
|
|
1116
|
+
...(exposed ? { exposed: true } : {}),
|
|
1117
|
+
});
|
|
866
1118
|
const approvers = this.approvers(request).filter((a) => a !== caller);
|
|
867
1119
|
this.#emit(() => this.#events.requested?.(request, approvers));
|
|
868
1120
|
});
|
|
@@ -919,7 +1171,7 @@ export class HubState {
|
|
|
919
1171
|
decline(caller, requestId) {
|
|
920
1172
|
const request = this.requests(caller).find((r) => r.requestId === requestId);
|
|
921
1173
|
if (!request)
|
|
922
|
-
throw new Refused("unknown", `No open request ${requestId} for ${caller} to decide.`);
|
|
1174
|
+
throw new Refused("unknown", `No open request ${requestId} for ${this.named(caller)} to decide; hub.grants.requests lists those it can.`);
|
|
923
1175
|
this.#transaction(() => {
|
|
924
1176
|
const [pending] = this.#pending("id = ?", requestId);
|
|
925
1177
|
if (pending)
|
|
@@ -21,10 +21,11 @@ export declare class Storage {
|
|
|
21
21
|
* Seed the store of (consumer, slot), all or none, if it holds nothing: `storage.admin@1`'s
|
|
22
22
|
* `seed`. Each entry has kv@1's limits, and together they count toward the consumer's quota.
|
|
23
23
|
*/
|
|
24
|
+
/** `named` is the consumer as a refusal names it: "Name (id)". */
|
|
24
25
|
seed(consumer: string, slot: string, entries: {
|
|
25
26
|
key: string;
|
|
26
27
|
value: unknown;
|
|
27
|
-
}[]): void;
|
|
28
|
+
}[], named?: string): void;
|
|
28
29
|
/** Every store of an Actor, gone: it was closed for good. */
|
|
29
30
|
deleteConsumer(consumer: string): void;
|
|
30
31
|
/**
|