@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.
Files changed (155) hide show
  1. package/README.md +15 -11
  2. package/dist/bundled/index.json +4 -4
  3. package/dist/bundled/{sha256-87e121cbe58279f3a633e61c9773b61fc17fd53eac3d7c6f037925cbfd5d5384.agentorg → sha256-0e91cc3d0e9762b81abc2427b5496b20f383abcd6fb97d928a8d50e956b54d1f.agentorg} +0 -0
  4. package/dist/bundled/sha256-1f64ca62a0209d767d6aa6c4feb50b0653d60f10bebbc301f79bd3a37e0153c1.agentorg +0 -0
  5. package/dist/bundled/{sha256-51ecf151e590f5e57f28229d4cb36e4932a2f1670e50ddf1a3ccf335a1ee146d.agentorg → sha256-4055f8fddf6b90f3d356543f554b2a9e9a2c4a8827a8774f8f87952dc1692331.agentorg} +0 -0
  6. package/dist/bundled/{sha256-b76ec31282e5b863ee9569dfd9d50b283a21101cbe1b3fac23d1cbcf16b9a408.agentorg → sha256-e356ea3c1899138a8494baf521556831006f38534fa9e92ae873e7e6956c3a46.agentorg} +0 -0
  7. package/{node_modules → dist/node_modules}/@agentorg/hub/dist/src/build.d.ts +2 -2
  8. package/{node_modules → dist/node_modules}/@agentorg/hub/dist/src/build.js +14 -4
  9. package/{node_modules → dist/node_modules}/@agentorg/hub/dist/src/hub-actor.js +41 -0
  10. package/{node_modules → dist/node_modules}/@agentorg/hub/dist/src/index.d.ts +1 -1
  11. package/{node_modules → dist/node_modules}/@agentorg/hub/dist/src/index.js +1 -1
  12. package/{node_modules → dist/node_modules}/@agentorg/hub/dist/src/router.js +40 -16
  13. package/{node_modules → dist/node_modules}/@agentorg/hub/dist/src/state.d.ts +87 -2
  14. package/{node_modules → dist/node_modules}/@agentorg/hub/dist/src/state.js +312 -60
  15. package/{node_modules → dist/node_modules}/@agentorg/hub/dist/src/storage.d.ts +2 -1
  16. package/{node_modules → dist/node_modules}/@agentorg/hub/dist/src/storage.js +3 -2
  17. package/{node_modules → dist/node_modules}/@agentorg/hub/package.json +3 -0
  18. package/{node_modules → dist/node_modules}/@agentorg/interfaces/dist/src/hub.d.ts +62 -3
  19. package/{node_modules → dist/node_modules}/@agentorg/interfaces/dist/src/hub.js +54 -3
  20. package/{node_modules → dist/node_modules}/@agentorg/interfaces/dist/src/index.d.ts +1 -0
  21. package/{node_modules → dist/node_modules}/@agentorg/interfaces/dist/src/index.js +1 -0
  22. package/{node_modules → dist/node_modules}/@agentorg/interfaces/dist/src/installer.d.ts +37 -0
  23. package/{node_modules → dist/node_modules}/@agentorg/interfaces/dist/src/installer.js +34 -3
  24. package/dist/node_modules/@agentorg/interfaces/dist/src/rules.d.ts +35 -0
  25. package/dist/node_modules/@agentorg/interfaces/dist/src/rules.js +37 -0
  26. package/dist/node_modules/@agentorg/kit/dist/src/chat.d.ts +37 -0
  27. package/dist/node_modules/@agentorg/kit/dist/src/chat.js +131 -0
  28. package/dist/node_modules/@agentorg/kit/dist/src/components.d.ts +94 -0
  29. package/dist/node_modules/@agentorg/kit/dist/src/components.js +66 -0
  30. package/dist/node_modules/@agentorg/kit/dist/src/icons.d.ts +17 -0
  31. package/dist/node_modules/@agentorg/kit/dist/src/icons.js +30 -0
  32. package/dist/node_modules/@agentorg/kit/dist/src/index.d.ts +6 -0
  33. package/dist/node_modules/@agentorg/kit/dist/src/index.js +10 -0
  34. package/dist/node_modules/@agentorg/kit/dist/src/markdown.d.ts +9 -0
  35. package/dist/node_modules/@agentorg/kit/dist/src/markdown.js +14 -0
  36. package/dist/node_modules/@agentorg/kit/dist/src/resizable.d.ts +5 -0
  37. package/dist/node_modules/@agentorg/kit/dist/src/resizable.js +8 -0
  38. package/dist/node_modules/@agentorg/kit/dist/src/styles.d.ts +11 -0
  39. package/dist/node_modules/@agentorg/kit/dist/src/styles.js +166 -0
  40. package/dist/node_modules/@agentorg/kit/package.json +44 -0
  41. package/{node_modules → dist/node_modules}/@agentorg/sdk/dist/frame/runtime.js +172 -17
  42. package/{node_modules → dist/node_modules}/@agentorg/sdk/dist/host/runner.js +168 -11
  43. package/{node_modules → dist/node_modules}/@agentorg/sdk/dist/src/client.d.ts +18 -0
  44. package/{node_modules → dist/node_modules}/@agentorg/sdk/dist/src/client.js +24 -3
  45. package/dist/node_modules/@agentorg/sdk/dist/src/frame-base.d.ts +8 -0
  46. package/dist/node_modules/@agentorg/sdk/dist/src/frame-base.js +64 -0
  47. package/{node_modules → dist/node_modules}/@agentorg/sdk/dist/src/frame-entry.js +4 -4
  48. package/{node_modules → dist/node_modules}/@agentorg/sdk/dist/src/frame-host.d.ts +1 -10
  49. package/{node_modules → dist/node_modules}/@agentorg/sdk/dist/src/frame-host.js +0 -23
  50. package/{node_modules → dist/node_modules}/@agentorg/sdk/dist/src/frame-protocol.d.ts +0 -8
  51. package/{node_modules → dist/node_modules}/@agentorg/sdk/dist/src/frame.d.ts +0 -2
  52. package/{node_modules → dist/node_modules}/@agentorg/sdk/dist/src/frame.js +0 -8
  53. package/{node_modules → dist/node_modules}/@agentorg/sdk/dist/src/host.js +5 -0
  54. package/{node_modules → dist/node_modules}/@agentorg/sdk/dist/src/index.d.ts +1 -1
  55. package/{node_modules → dist/node_modules}/@agentorg/sdk/dist/src/module-host.d.ts +6 -1
  56. package/{node_modules → dist/node_modules}/@agentorg/sdk/dist/src/module-host.js +24 -1
  57. package/{node_modules → dist/node_modules}/@agentorg/sdk/dist/src/module.d.ts +30 -1
  58. package/{node_modules → dist/node_modules}/@agentorg/sdk/dist/src/package.js +111 -6
  59. package/{node_modules → dist/node_modules}/@agentorg/sdk/dist/src/runtime.d.ts +22 -1
  60. package/{node_modules → dist/node_modules}/@agentorg/sdk/dist/src/runtime.js +114 -0
  61. package/{node_modules → dist/node_modules}/@agentorg/sdk/dist/src/testing.d.ts +14 -2
  62. package/dist/node_modules/@agentorg/sdk/dist/src/testing.js +89 -0
  63. package/{node_modules → dist/node_modules}/@agentorg/shell/dist/app.css +27 -144
  64. package/dist/node_modules/@agentorg/shell/dist/app.js +219 -0
  65. package/{node_modules → dist/node_modules}/@agentorg/shell/package.json +1 -0
  66. package/{node_modules → dist/node_modules}/@agentorg/wire/dist/src/index.d.ts +1 -1
  67. package/{node_modules → dist/node_modules}/@agentorg/wire/dist/src/ops.d.ts +14 -1
  68. package/{node_modules → dist/node_modules}/@agentorg/wire/dist/src/ops.js +22 -2
  69. package/dist/src/cli.js +23 -0
  70. package/dist/src/gateway.js +42 -1
  71. package/dist/src/model.d.ts +7 -4
  72. package/dist/src/model.js +83 -14
  73. package/dist/src/rules.d.ts +71 -0
  74. package/dist/src/rules.js +157 -0
  75. package/package.json +13 -16
  76. package/dist/bundled/sha256-e4c0e857cea3a721998effe2ca859c4011978b9bc36e63c058bfc17c27a1772e.agentorg +0 -0
  77. package/node_modules/@agentorg/sdk/dist/src/testing.js +0 -51
  78. package/node_modules/@agentorg/shell/dist/app.js +0 -9
  79. /package/{node_modules → dist/node_modules}/@agentorg/hub/dist/src/hub-actor.d.ts +0 -0
  80. /package/{node_modules → dist/node_modules}/@agentorg/hub/dist/src/logs.d.ts +0 -0
  81. /package/{node_modules → dist/node_modules}/@agentorg/hub/dist/src/logs.js +0 -0
  82. /package/{node_modules → dist/node_modules}/@agentorg/hub/dist/src/net.d.ts +0 -0
  83. /package/{node_modules → dist/node_modules}/@agentorg/hub/dist/src/net.js +0 -0
  84. /package/{node_modules → dist/node_modules}/@agentorg/hub/dist/src/processes.d.ts +0 -0
  85. /package/{node_modules → dist/node_modules}/@agentorg/hub/dist/src/processes.js +0 -0
  86. /package/{node_modules → dist/node_modules}/@agentorg/hub/dist/src/router.d.ts +0 -0
  87. /package/{node_modules → dist/node_modules}/@agentorg/hub/dist/src/server.d.ts +0 -0
  88. /package/{node_modules → dist/node_modules}/@agentorg/hub/dist/src/server.js +0 -0
  89. /package/{node_modules → dist/node_modules}/@agentorg/hub/dist/src/validate-worker.d.ts +0 -0
  90. /package/{node_modules → dist/node_modules}/@agentorg/hub/dist/src/validate-worker.js +0 -0
  91. /package/{node_modules → dist/node_modules}/@agentorg/hub/dist/src/validate.d.ts +0 -0
  92. /package/{node_modules → dist/node_modules}/@agentorg/hub/dist/src/validate.js +0 -0
  93. /package/{node_modules → dist/node_modules}/@agentorg/interfaces/dist/src/account-login.d.ts +0 -0
  94. /package/{node_modules → dist/node_modules}/@agentorg/interfaces/dist/src/account-login.js +0 -0
  95. /package/{node_modules → dist/node_modules}/@agentorg/interfaces/dist/src/chat.d.ts +0 -0
  96. /package/{node_modules → dist/node_modules}/@agentorg/interfaces/dist/src/chat.js +0 -0
  97. /package/{node_modules → dist/node_modules}/@agentorg/interfaces/dist/src/http-egress.d.ts +0 -0
  98. /package/{node_modules → dist/node_modules}/@agentorg/interfaces/dist/src/http-egress.js +0 -0
  99. /package/{node_modules → dist/node_modules}/@agentorg/interfaces/dist/src/kv.d.ts +0 -0
  100. /package/{node_modules → dist/node_modules}/@agentorg/interfaces/dist/src/kv.js +0 -0
  101. /package/{node_modules → dist/node_modules}/@agentorg/interfaces/dist/src/layout.d.ts +0 -0
  102. /package/{node_modules → dist/node_modules}/@agentorg/interfaces/dist/src/layout.js +0 -0
  103. /package/{node_modules → dist/node_modules}/@agentorg/interfaces/dist/src/llm-session.d.ts +0 -0
  104. /package/{node_modules → dist/node_modules}/@agentorg/interfaces/dist/src/llm-session.js +0 -0
  105. /package/{node_modules → dist/node_modules}/@agentorg/interfaces/dist/src/storage-admin.d.ts +0 -0
  106. /package/{node_modules → dist/node_modules}/@agentorg/interfaces/dist/src/storage-admin.js +0 -0
  107. /package/{node_modules → dist/node_modules}/@agentorg/interfaces/dist/src/ui-shell.d.ts +0 -0
  108. /package/{node_modules → dist/node_modules}/@agentorg/interfaces/dist/src/ui-shell.js +0 -0
  109. /package/{node_modules → dist/node_modules}/@agentorg/interfaces/dist/src/ui-view.d.ts +0 -0
  110. /package/{node_modules → dist/node_modules}/@agentorg/interfaces/dist/src/ui-view.js +0 -0
  111. /package/{node_modules → dist/node_modules}/@agentorg/interfaces/package.json +0 -0
  112. /package/{node_modules → dist/node_modules}/@agentorg/sdk/dist/src/build.d.ts +0 -0
  113. /package/{node_modules → dist/node_modules}/@agentorg/sdk/dist/src/build.js +0 -0
  114. /package/{node_modules → dist/node_modules}/@agentorg/sdk/dist/src/canonical.d.ts +0 -0
  115. /package/{node_modules → dist/node_modules}/@agentorg/sdk/dist/src/canonical.js +0 -0
  116. /package/{node_modules → dist/node_modules}/@agentorg/sdk/dist/src/cli.d.ts +0 -0
  117. /package/{node_modules → dist/node_modules}/@agentorg/sdk/dist/src/cli.js +0 -0
  118. /package/{node_modules → dist/node_modules}/@agentorg/sdk/dist/src/client-http.d.ts +0 -0
  119. /package/{node_modules → dist/node_modules}/@agentorg/sdk/dist/src/client-http.js +0 -0
  120. /package/{node_modules → dist/node_modules}/@agentorg/sdk/dist/src/describe.d.ts +0 -0
  121. /package/{node_modules → dist/node_modules}/@agentorg/sdk/dist/src/describe.js +0 -0
  122. /package/{node_modules → dist/node_modules}/@agentorg/sdk/dist/src/errors.d.ts +0 -0
  123. /package/{node_modules → dist/node_modules}/@agentorg/sdk/dist/src/errors.js +0 -0
  124. /package/{node_modules → dist/node_modules}/@agentorg/sdk/dist/src/frame-entry.d.ts +0 -0
  125. /package/{node_modules → dist/node_modules}/@agentorg/sdk/dist/src/frame-protocol.js +0 -0
  126. /package/{node_modules → dist/node_modules}/@agentorg/sdk/dist/src/host-main.d.ts +0 -0
  127. /package/{node_modules → dist/node_modules}/@agentorg/sdk/dist/src/host-main.js +0 -0
  128. /package/{node_modules → dist/node_modules}/@agentorg/sdk/dist/src/host.d.ts +0 -0
  129. /package/{node_modules → dist/node_modules}/@agentorg/sdk/dist/src/index.js +0 -0
  130. /package/{node_modules → dist/node_modules}/@agentorg/sdk/dist/src/interface.d.ts +0 -0
  131. /package/{node_modules → dist/node_modules}/@agentorg/sdk/dist/src/interface.js +0 -0
  132. /package/{node_modules → dist/node_modules}/@agentorg/sdk/dist/src/manifest-api.d.ts +0 -0
  133. /package/{node_modules → dist/node_modules}/@agentorg/sdk/dist/src/manifest-api.js +0 -0
  134. /package/{node_modules → dist/node_modules}/@agentorg/sdk/dist/src/manifest.d.ts +0 -0
  135. /package/{node_modules → dist/node_modules}/@agentorg/sdk/dist/src/manifest.js +0 -0
  136. /package/{node_modules → dist/node_modules}/@agentorg/sdk/dist/src/module.js +0 -0
  137. /package/{node_modules → dist/node_modules}/@agentorg/sdk/dist/src/package.d.ts +0 -0
  138. /package/{node_modules → dist/node_modules}/@agentorg/sdk/dist/src/person-commands.d.ts +0 -0
  139. /package/{node_modules → dist/node_modules}/@agentorg/sdk/dist/src/person-commands.js +0 -0
  140. /package/{node_modules → dist/node_modules}/@agentorg/sdk/dist/src/tar.d.ts +0 -0
  141. /package/{node_modules → dist/node_modules}/@agentorg/sdk/dist/src/tar.js +0 -0
  142. /package/{node_modules → dist/node_modules}/@agentorg/sdk/package.json +0 -0
  143. /package/{node_modules → dist/node_modules}/@agentorg/shell/dist/agent-bindings.js +0 -0
  144. /package/{node_modules → dist/node_modules}/@agentorg/shell/dist/index.html +0 -0
  145. /package/{node_modules → dist/node_modules}/@agentorg/shell/src/agent-bindings.ts +0 -0
  146. /package/{node_modules → dist/node_modules}/@agentorg/wire/dist/src/bytes.d.ts +0 -0
  147. /package/{node_modules → dist/node_modules}/@agentorg/wire/dist/src/bytes.js +0 -0
  148. /package/{node_modules → dist/node_modules}/@agentorg/wire/dist/src/frames.d.ts +0 -0
  149. /package/{node_modules → dist/node_modules}/@agentorg/wire/dist/src/frames.js +0 -0
  150. /package/{node_modules → dist/node_modules}/@agentorg/wire/dist/src/index.js +0 -0
  151. /package/{node_modules → dist/node_modules}/@agentorg/wire/dist/src/transports.d.ts +0 -0
  152. /package/{node_modules → dist/node_modules}/@agentorg/wire/dist/src/transports.js +0 -0
  153. /package/{node_modules → dist/node_modules}/@agentorg/wire/dist/src/yamux.d.ts +0 -0
  154. /package/{node_modules → dist/node_modules}/@agentorg/wire/dist/src/yamux.js +0 -0
  155. /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 [module, m] of Object.entries(manifest.modules))
353
- for (const [side, members] of [
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; it connects itself.`);
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 bound name keeps its interface: redeclaring it as another is
566
- * refused. A name it no longer declares is dropped, with its bindings.
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.#isBound(actorId, side, name) &&
584
- compareInterfaces(known.iface, iface) !== "same")
585
- throw new Refused("conflict", `${name} is bound as ${known.iface.id}; it can't change while bound.`);
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("SELECT * FROM bindings WHERE consumer = ? AND slot = ?")
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("SELECT * FROM bindings WHERE consumer = ? OR provider = ? ORDER BY rowid")
855
+ .prepare(`SELECT ${BINDING} FROM bindings b WHERE consumer = ? OR provider = ? ORDER BY b.rowid`)
695
856
  .all(actor, actor)
696
- : this.#db.prepare("SELECT * FROM bindings ORDER BY rowid").all();
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 * FROM bindings WHERE ${where}`).all(...params).map(bindingOf);
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
- this.#ends(wiring);
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", { ...wiring, needs, requestId: request.requestId });
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
  /**