@fungi.computer/oyster 0.6.0 → 0.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (163) hide show
  1. package/README.md +133 -78
  2. package/dist/agent-archive-projection.d.ts +8 -0
  3. package/dist/agent-archive-projection.js +49 -0
  4. package/dist/agent-schedules.d.ts +61 -0
  5. package/dist/agent-schedules.js +19 -0
  6. package/dist/agents.d.ts +35 -14
  7. package/dist/agents.js +93 -144
  8. package/dist/app-builds.d.ts +7 -2
  9. package/dist/app-builds.js +19 -2
  10. package/dist/approval-capability.d.ts +1 -1
  11. package/dist/approval-capability.js +11 -16
  12. package/dist/approval-home.d.ts +2 -1
  13. package/dist/approval-home.js +5 -3
  14. package/dist/apps/catalog.js +1 -1
  15. package/dist/apps/gate.d.ts +4 -7
  16. package/dist/apps/gate.js +14 -29
  17. package/dist/apps/index.d.ts +1 -1
  18. package/dist/apps/installation.d.ts +4 -42
  19. package/dist/apps/installation.js +7 -75
  20. package/dist/apps/operations.js +0 -10
  21. package/dist/apps/permissions.d.ts +18 -8
  22. package/dist/apps/permissions.js +7 -1
  23. package/dist/auth.d.ts +1 -24
  24. package/dist/auth.js +9 -39
  25. package/dist/authorization.d.ts +307 -0
  26. package/dist/authorization.js +542 -0
  27. package/dist/boot-host-script.js +1 -1
  28. package/dist/build/build-jobs.d.ts +2 -2
  29. package/dist/build/candidate-builder.d.ts +2 -0
  30. package/dist/build/candidate-builder.js +5 -1
  31. package/dist/build/candidate-cli.js +3 -0
  32. package/dist/build/candidate-cli.mjs +70 -22
  33. package/dist/build/job-log.d.ts +23 -0
  34. package/dist/build/job-log.js +88 -0
  35. package/dist/build/local-build.d.ts +9 -1
  36. package/dist/build/local-build.js +11 -9
  37. package/dist/cloudflare.d.ts +4 -1
  38. package/dist/cloudflare.js +1 -1
  39. package/dist/files.js +9 -8
  40. package/dist/git-admission.d.ts +3 -3
  41. package/dist/git-admission.js +4 -6
  42. package/dist/git-door.d.ts +8 -7
  43. package/dist/git-door.js +18 -17
  44. package/dist/git.d.ts +1 -1
  45. package/dist/host.d.ts +33 -40
  46. package/dist/host.js +156 -275
  47. package/dist/installation-execution.js +14 -5
  48. package/dist/installation-operations.d.ts +4 -180
  49. package/dist/installation-operations.js +1 -5
  50. package/dist/installation-store-generation.d.ts +4 -1
  51. package/dist/installation-store-generation.js +59 -6
  52. package/dist/internal/delivery.js +0 -1
  53. package/dist/internal/host-gate.d.ts +3 -2
  54. package/dist/internal/host-gate.js +5 -18
  55. package/dist/internal/invocation-timing.d.ts +16 -0
  56. package/dist/internal/invocation-timing.js +23 -0
  57. package/dist/internal/registry-installations.js +1 -0
  58. package/dist/invocation.d.ts +15 -14
  59. package/dist/invocation.js +58 -26
  60. package/dist/keys.d.ts +2 -19
  61. package/dist/keys.js +1 -37
  62. package/dist/mcp.d.ts +15 -27
  63. package/dist/mcp.js +105 -155
  64. package/dist/page/agent-browser.d.ts +15 -0
  65. package/dist/page/agent-browser.js +162 -0
  66. package/dist/page/app-permissions-port.d.ts +3 -0
  67. package/dist/page/app-permissions-port.js +43 -0
  68. package/dist/page/app-permissions.d.ts +11 -0
  69. package/dist/page/app-permissions.js +296 -0
  70. package/dist/page/isolated-page-theme.d.ts +2 -0
  71. package/dist/page/isolated-page-theme.js +11 -0
  72. package/dist/page/mount.d.ts +2 -13
  73. package/dist/page/mount.js +7 -93
  74. package/dist/page-script.d.ts +0 -3
  75. package/dist/page-script.js +3 -4
  76. package/dist/standalone/agent-archive-owner.d.ts +42 -0
  77. package/dist/standalone/agent-archive-owner.js +116 -0
  78. package/dist/standalone/agent-archive-schedules.d.ts +6 -0
  79. package/dist/standalone/agent-archive-schedules.js +76 -0
  80. package/dist/standalone/agent-archive.d.ts +31 -0
  81. package/dist/standalone/agent-archive.js +67 -0
  82. package/dist/standalone/agent-computers.d.ts +7 -6
  83. package/dist/standalone/agent-computers.js +4 -7
  84. package/dist/standalone/agent-mail.d.ts +2 -2
  85. package/dist/standalone/agents.d.ts +186 -20
  86. package/dist/standalone/agents.js +251 -30
  87. package/dist/standalone/app-builds-binding.js +1 -1
  88. package/dist/standalone/app-set.js +2 -54
  89. package/dist/standalone/app-source.d.ts +3 -4
  90. package/dist/standalone/app-source.js +12 -9
  91. package/dist/standalone/builds.d.ts +2 -14
  92. package/dist/standalone/builds.js +15 -13
  93. package/dist/standalone/computer-fleet.d.ts +4 -4
  94. package/dist/standalone/computer-fleet.js +13 -42
  95. package/dist/standalone/computer.d.ts +4 -3
  96. package/dist/standalone/computer.js +52 -53
  97. package/dist/standalone/dashboard-approval.d.ts +10 -0
  98. package/dist/standalone/dashboard-approval.js +66 -0
  99. package/dist/standalone/machine-link.d.ts +4 -221
  100. package/dist/standalone/machine-link.js +21 -15
  101. package/dist/standalone/media.d.ts +1 -0
  102. package/dist/standalone/media.js +20 -14
  103. package/dist/standalone/owner-routes.d.ts +16 -0
  104. package/dist/standalone/owner-routes.js +21 -0
  105. package/dist/standalone/permission-resources.d.ts +5 -0
  106. package/dist/standalone/permission-resources.js +31 -0
  107. package/dist/standalone/registry.d.ts +7 -0
  108. package/dist/standalone/registry.js +3 -0
  109. package/dist/standalone/tenancy.d.ts +0 -3
  110. package/dist/standalone/tenancy.js +2 -5
  111. package/dist/standalone/worker-routes.d.ts +19 -0
  112. package/dist/standalone/worker-routes.js +9 -0
  113. package/dist/standalone/worker.d.ts +1 -1
  114. package/dist/standalone/worker.js +27 -35
  115. package/dist/standalone/workflow-binding.d.ts +10 -3
  116. package/dist/standalone/workflow-binding.js +22 -5
  117. package/dist/standalone/workflow-git-capture.js +2 -1
  118. package/dist/standalone/workflow-host.d.ts +8 -103
  119. package/dist/standalone/workflow-host.js +1 -1
  120. package/dist/standalone/workflow-node-capture.js +2 -2
  121. package/dist/standalone/workflow-root-authority.d.ts +1 -1
  122. package/dist/standalone/workflow-root-authority.js +1 -1
  123. package/dist/standalone/workflow-schedules.d.ts +31 -4
  124. package/dist/standalone/workflow-schedules.js +18 -5
  125. package/dist/standalone/workflow-webhooks.d.ts +4 -18
  126. package/dist/standalone/workflow-webhooks.js +5 -5
  127. package/dist/standalone/workflows.d.ts +53 -27
  128. package/dist/standalone/workflows.js +9 -3
  129. package/dist/storage-frame.d.ts +6 -6
  130. package/dist/testing/capabilities.d.ts +7 -6
  131. package/dist/testing/capabilities.js +3 -6
  132. package/dist/testing/installation.d.ts +0 -1
  133. package/dist/testing/installation.js +2 -5
  134. package/dist/workers-git.d.ts +12 -4
  135. package/dist/workers-git.js +38 -35
  136. package/dist/workers.d.ts +9 -1
  137. package/dist/workers.js +44 -8
  138. package/dist/workflows.d.ts +4 -0
  139. package/dist/workflows.js +2 -0
  140. package/docs/api/README.md +7 -0
  141. package/docs/api/agent-schedules.md +176 -0
  142. package/docs/api/agents.md +34 -13
  143. package/docs/api/app-storage.md +1 -1
  144. package/docs/api/apps.md +323 -225
  145. package/docs/api/auth.md +0 -55
  146. package/docs/api/authorization.md +1300 -0
  147. package/docs/api/build.md +42 -38
  148. package/docs/api/cloudflare.md +3 -3
  149. package/docs/api/git.md +20 -11
  150. package/docs/api/host.md +64 -192
  151. package/docs/api/index.md +1 -1
  152. package/docs/api/invocation.md +14 -14
  153. package/docs/api/keys.md +1 -98
  154. package/docs/api/mcp.md +43 -40
  155. package/docs/api/modules.md +2 -0
  156. package/docs/api/page/mount.md +16 -19
  157. package/docs/api/workers-git.md +46 -12
  158. package/docs/api/workers.md +111 -222
  159. package/package.json +41 -22
  160. package/dist/page/computer-target.d.ts +0 -3
  161. package/dist/page/computer-target.js +0 -9
  162. package/dist/standalone/channel-session-door.d.ts +0 -4
  163. package/dist/standalone/channel-session-door.js +0 -72
@@ -2,12 +2,11 @@ import { isRetryableProviderRejection, normalizeModel, observeProviderStatus, st
2
2
  import { standaloneTenancy, } from "./tenancy.js";
3
3
  import { AcceptedSchema } from "@fungi.computer/shiitake/client";
4
4
  import { createShiitakeClient } from "@fungi.computer/shiitake/client";
5
- import { AgentCreateRefused, } from "@fungi.computer/shiitake/agent-control";
5
+ import { ShiitakeError } from "@fungi.computer/shiitake";
6
+ import { AgentArchiveRefused, AgentCreateRefused, } from "@fungi.computer/shiitake/agent-control";
6
7
  import { AgentDelegation, AgentDisclosureRefused, machineAgentCodecs, } from "@fungi.computer/shiitake/delegation";
7
8
  import { FungiMessageMetaSchema } from "@fungi.computer/agar/agents";
8
9
  import { agentMail } from "./agent-mail.js";
9
- import { createChannelConversations, } from "../channels/conversations.js";
10
- import { channelSessionDoor } from "./channel-session-door.js";
11
10
  import { createStandaloneMachineEnrollment, standaloneMachineAgentHome, standaloneMachineLinkAuthority, } from "./machine-link.js";
12
11
  import { createMachineLinkFences, MachineEnrollmentRefused, MachineLinkRefused, createMachineAgentClient, } from "../machine-link/index.js";
13
12
  import { memberInboxItemSchema } from "@fungi.computer/puffball/inbox";
@@ -28,6 +27,8 @@ import { homeContract } from "@fungi.computer/shiitake-extensions/home";
28
27
  import { DurableObject } from "cloudflare:workers";
29
28
  import { ShiitakeDO } from "@fungi.computer/shiitake/cloudflare";
30
29
  import { agentStoreKeeping } from "./agent-store.js";
30
+ import { agentArchiveOwner } from "./agent-archive-owner.js";
31
+ import { agentArchiveProjection } from "../agent-archive-projection.js";
31
32
  import { standaloneBrowserSource } from "./browser.js";
32
33
  import { configuredProviderEgress, providerFetch, } from "@fungi.computer/veil/gatekeeper";
33
34
  import { createAssistantMessageEventStream } from "@earendil-works/pi-ai/utils/event-stream";
@@ -38,13 +39,18 @@ import * as Schema from "effect/Schema";
38
39
  import { z } from "zod";
39
40
  import {} from "../cloudflare.js";
40
41
  import { AppInstanceError } from "../index.js";
42
+ import { agentScheduleBindingArgs, appAgentScheduleListInputSchema, agentSchedulePageSchema, agentScheduleControlReceiptSchema, } from "../agent-schedules.js";
43
+ import { readAgentSchedulePage, controlAgentSchedule, } from "@fungi.computer/shiitake-extensions/schedules/client";
44
+ import { agentScheduleTargetSchema } from "@fungi.computer/shiitake-extensions/schedules";
41
45
  import { createAgentAvatarStore, } from "../agent-avatars.js";
42
46
  import { defineIdFamily } from "@fungi.computer/cairn";
43
47
  import { agentIds, createAgentDirectory, createFleetSessions, commandSettlementSchema, DirectoryError, } from "@fungi.computer/shiitake/fleet";
44
48
  import { appToolPolicySchema } from "../apps/index.js";
45
- import { oysterPrincipalIsOwner, oysterPrincipalId, } from "../host.js";
49
+ import { oysterPrincipalId } from "../host.js";
50
+ import { oysterPrincipalIsOwner } from "../authorization.js";
46
51
  import { defaultTenant, tenantIdSchema, tenantScope, } from "../tenant.js";
47
52
  import { createCodemodeSandbox } from "@fungi.computer/mycelium/workers";
53
+ import { forageDiscoveryInstructions } from "@fungi.computer/mycelium/effect";
48
54
  import { maxAgentMcpServers, } from "../agents.js";
49
55
  /** Own transport evidence and exact-message decisions for one model lease. */
50
56
  function observedModels(model, defaults) {
@@ -219,6 +225,7 @@ const toMcpServer = (stored) => ({
219
225
  hasBearer: stored.bearer !== null,
220
226
  });
221
227
  const instructions = [
228
+ forageDiscoveryInstructions,
222
229
  "Attached Computers are independent env.computers.<name> modules, including their Files and execute operations; no App is needed. Discover their exact operations before calling.",
223
230
  "You are an agent on a personal Oyster: a small, self-hosted home for Apps.",
224
231
  "Your tool is execute: it runs JavaScript in an isolated sandbox. Each installed App is an env.<APP> module; dotted method names are nested properties, e.g. `return await env.BONSAI.repos.list({})`.",
@@ -229,22 +236,29 @@ const instructions = [
229
236
  ].join("\n");
230
237
  /** One agent: Shiitake's Cloudflare host over this Durable Object; its root supplies `hostFor`. */
231
238
  export class StandaloneAgentBase extends ShiitakeDO {
232
- /**
233
- * Compose Oyster's channel owner on this Agent's real store and Shiitake.
234
- * A host must supply its own binding lookup after authenticating its edge;
235
- * this does not install an inbound route or mint provider credentials.
236
- */
237
- channelConversations(binding) {
238
- const owner = createChannelConversations({
239
- sql: this.ctx.storage.sql,
240
- transactionSync: (body) => this.ctx.storage.transactionSync(body),
241
- binding,
242
- door: channelSessionDoor(() => this.shiitake(), () => {
243
- // Standalone Agents have no Hub Team grants to deliver.
244
- }, () => owner),
245
- now: () => Date.now(),
246
- });
247
- return owner;
239
+ archiveControlOwner;
240
+ archiveControl() {
241
+ return (this.archiveControlOwner ??= agentArchiveOwner({
242
+ storage: this.ctx.storage,
243
+ agentId: () => this.ctx.id.name,
244
+ agent: () => this.shiitake(),
245
+ scheduledExtensions: true,
246
+ refreshOutstanding: (agentId) => this.delegation().refreshOutstanding((target, sessionId, commandId) => this.machineClient().then((client) => client.settlement(target, sessionId, commandId, agentId))),
247
+ record: async (agentId, archived, revision) => {
248
+ const pin = await this.policyDirectory();
249
+ if (pin === null)
250
+ return false;
251
+ return this.env.AGENT_DIRECTORY.getByName(pin.directoryName).recordArchive(agentId, archived, revision);
252
+ },
253
+ wake: () => this.wakeRuntime(),
254
+ waitUntil: (task) => {
255
+ this.ctx.waitUntil(task);
256
+ },
257
+ }));
258
+ }
259
+ /** Owner-local atomic transition. The caller checks human/creator authority. */
260
+ setArchived(archived) {
261
+ return this.archiveControl().setArchived(archived);
248
262
  }
249
263
  /** Each installed Release's verified catalog surface, built once per Release for this Agent's lifetime. */
250
264
  #appSurfaces;
@@ -389,17 +403,40 @@ export class StandaloneAgentBase extends ShiitakeDO {
389
403
  }
390
404
  }
391
405
  controlDirectory() {
392
- const list = async (signal) => {
406
+ const list = async (signal, options) => {
393
407
  signal.throwIfAborted();
394
408
  const pin = await this.policyDirectory();
395
409
  if (pin === null)
396
410
  throw new Error("Agent directory is unavailable");
397
- const agents = await this.env.AGENT_DIRECTORY.getByName(pin.directoryName).list();
398
- if (!agents.some((agent) => agent.agentId === this.ctx.id.name))
411
+ const directory = this.env.AGENT_DIRECTORY.getByName(pin.directoryName);
412
+ const agents = await directory.list(options);
413
+ if (this.ctx.id.name === undefined ||
414
+ !(await directory.has(this.ctx.id.name)))
399
415
  throw new Error("Agent is outside this scope");
400
416
  signal.throwIfAborted();
401
417
  return agents;
402
418
  };
419
+ const changeArchive = async (targetId, archived, signal) => {
420
+ await list(signal);
421
+ const caller = this.ctx.id.name;
422
+ const pin = await this.policyDirectory();
423
+ if (caller === undefined || pin === null)
424
+ throw new AgentArchiveRefused("unreadable", "Agent scope is unavailable");
425
+ if (targetId === caller)
426
+ throw new AgentArchiveRefused("self", "An Agent cannot archive or restore itself");
427
+ const directory = this.env.AGENT_DIRECTORY.getByName(pin.directoryName);
428
+ if ((await directory.createdBy(targetId)) !== caller)
429
+ throw new AgentArchiveRefused("not_creator", "An Agent can change only one it created");
430
+ const target = this.env.AGENTS.getByName(targetId);
431
+ const targetPin = await target.policyDirectory();
432
+ if (targetPin?.directoryName !== pin.directoryName ||
433
+ targetPin.tenantId !== pin.tenantId)
434
+ throw new AgentArchiveRefused("unreadable", "Agent archive scope does not match");
435
+ signal.throwIfAborted();
436
+ const result = await target.setArchived(archived);
437
+ if ("error" in result)
438
+ throw new AgentArchiveRefused(result.error, result.message ?? `Agent archive refused: ${result.error}`);
439
+ };
403
440
  return {
404
441
  list,
405
442
  create: async (input, signal) => {
@@ -420,10 +457,17 @@ export class StandaloneAgentBase extends ShiitakeDO {
420
457
  },
421
458
  send: (input, sessionId, signal, invocation) => this.delegation().send(input, sessionId, signal, invocation),
422
459
  recall: (input, sessionId, signal) => this.delegation().recall(input, sessionId, signal),
460
+ archive: (input, signal) => changeArchive(input.agentId, true, signal),
461
+ unarchive: (input, signal) => changeArchive(input.agentId, false, signal),
423
462
  open: async (agentId, signal) => {
424
463
  const admit = async () => {
425
- if (!(await list(signal)).some((agent) => agent.agentId === agentId))
464
+ if (!(await list(signal)).some((agent) => agent.agentId === agentId)) {
465
+ const pin = await this.policyDirectory();
466
+ if (pin !== null &&
467
+ (await this.env.AGENT_DIRECTORY.getByName(pin.directoryName).archived(agentId)))
468
+ throw new ShiitakeError("contract", "archived", "Agent is archived");
426
469
  throw new Error("Agent is outside this scope");
470
+ }
427
471
  };
428
472
  await admit();
429
473
  const agent = createShiitakeClient({
@@ -454,7 +498,19 @@ export class StandaloneAgentBase extends ShiitakeDO {
454
498
  ? null
455
499
  : { scopeId: pin.directoryName, agentId };
456
500
  },
457
- directory: () => this.controlDirectory(),
501
+ // Delegation custody and settlement still see archived identities;
502
+ // Agent-control discovery keeps its active-only default. New sends
503
+ // reach the recipient's admission door and get contract/archived.
504
+ directory: () => {
505
+ const control = this.controlDirectory();
506
+ return {
507
+ ...control,
508
+ list: async (signal) => [
509
+ ...(await control.list(signal)),
510
+ ...(await control.list(signal, { archived: true })),
511
+ ],
512
+ };
513
+ },
458
514
  deliver: (mail) => this.mail().deliver(mail),
459
515
  prepare: () => Promise.resolve(),
460
516
  stageAcceptedNotice: (mail, scopeId) => {
@@ -632,6 +688,28 @@ export class StandaloneAgentBase extends ShiitakeDO {
632
688
  ? null
633
689
  : Schema.decodeUnknownSync(policyDirectorySchema)(stored);
634
690
  }
691
+ /** Borrow the native owner only after matching its real initialization pin. */
692
+ async scheduleOwner(expected, agentId) {
693
+ const directory = Schema.decodeUnknownSync(policyDirectorySchema)(expected);
694
+ const pin = await this.policyDirectory();
695
+ if (agentId !== this.ctx.id.name ||
696
+ pin?.directoryName !== directory.directoryName ||
697
+ pin.tenantId !== directory.tenantId)
698
+ throw new AppInstanceError("revoked");
699
+ return this.shiitake();
700
+ }
701
+ /** One admitted Agent's bounded native schedule page; this is not a directory scan. */
702
+ async readSchedules(directory, input) {
703
+ const { agentId, ...page } = appAgentScheduleListInputSchema.parse(input);
704
+ const owner = await this.scheduleOwner(directory, agentId);
705
+ return readAgentSchedulePage(agentId, owner, page);
706
+ }
707
+ /** Manage authority returns native identity/state only, never schedule content. */
708
+ async controlSchedule(directory, input, action) {
709
+ const target = agentScheduleTargetSchema.parse(input);
710
+ const owner = await this.scheduleOwner(directory, target.agentId);
711
+ return controlAgentSchedule(owner, target, action);
712
+ }
635
713
  /** Listeners from live codemode acquisitions, told when the server list changes. */
636
714
  mcpInvalidations = new Set();
637
715
  async storedMcpServers() {
@@ -697,7 +775,13 @@ export class StandaloneAgentBase extends ShiitakeDO {
697
775
  meta: { schema: FungiMessageMetaSchema },
698
776
  ...this.mail().host,
699
777
  instructions: {
700
- load: () => Promise.resolve({ revision: "oyster-5", text: instructions }),
778
+ load: async () => {
779
+ const { appApprovalInstructions } = await import("../mcp.js");
780
+ return {
781
+ revision: "oyster-7",
782
+ text: instructions + "\n" + appApprovalInstructions,
783
+ };
784
+ },
701
785
  },
702
786
  tools: {
703
787
  sources: [
@@ -777,7 +861,16 @@ export class StandaloneAgentBase extends ShiitakeDO {
777
861
  grants: [...extensions.grants, homeGrant],
778
862
  },
779
863
  ...keeping,
780
- hostWork: [...this.mail().host.hostWork, ...keeping.hostWork],
864
+ admission: {
865
+ assertOpen: () => {
866
+ this.archiveControl().assertOpen();
867
+ },
868
+ },
869
+ hostWork: [
870
+ ...this.mail().host.hostWork,
871
+ ...keeping.hostWork,
872
+ this.archiveControl().work,
873
+ ],
781
874
  };
782
875
  }
783
876
  /** Send an installation-admitted message through fleet Session operations. */
@@ -841,6 +934,27 @@ const visitRow = Schema.Struct({
841
934
  const counterRow = Schema.Struct({ value: Schema.Number });
842
935
  /** Names this Oyster's agents and remembers which Sessions the member opened. */
843
936
  export class AgentDirectory extends DurableObject {
937
+ archiveProjectionOwner;
938
+ archiveProjection() {
939
+ return (this.archiveProjectionOwner ??= agentArchiveProjection(this.ctx.storage.sql));
940
+ }
941
+ /** Apply the Agent DO's revisioned fact; an older repair cannot undo a newer one. */
942
+ recordArchive(agentId, archived, revision) {
943
+ const projection = this.archiveProjection();
944
+ return this.ctx.storage.transactionSync(() => {
945
+ if (!this.has(agentId))
946
+ return false;
947
+ const result = projection.record(agentId, archived, revision);
948
+ if (result === "advanced")
949
+ this.bump();
950
+ // A replay after projection but before the Agent's ack still succeeds.
951
+ return result !== "stale";
952
+ });
953
+ }
954
+ /** Directory's bounded-stale projection, never the Agent's flag authority. */
955
+ archived(agentId) {
956
+ return this.archiveProjection().archived(agentId);
957
+ }
844
958
  computers;
845
959
  computerNames = new Map();
846
960
  directory;
@@ -1172,10 +1286,14 @@ export class AgentDirectory extends DurableObject {
1172
1286
  provenance: { actor: oysterPrincipalId(owner), via: captured.agentId },
1173
1287
  };
1174
1288
  }
1175
- list() {
1289
+ list(options) {
1290
+ const archived = this.archiveProjection().archivedIds();
1176
1291
  return this.directory
1177
1292
  .list(this.grant())
1178
1293
  .toReversed()
1294
+ .filter(({ agentId }) => options?.archived === true
1295
+ ? archived.has(agentId)
1296
+ : !archived.has(agentId))
1179
1297
  .map(({ agentId, name, createdBy }) => ({
1180
1298
  agentId,
1181
1299
  createdBy,
@@ -1191,6 +1309,13 @@ export class AgentDirectory extends DurableObject {
1191
1309
  readCreator(agentId) {
1192
1310
  return this.directory.readCreator(this.grant(), agentId);
1193
1311
  }
1312
+ /** Immutable creating Agent, distinct from the human credential owner. */
1313
+ createdBy(agentId) {
1314
+ const id = agentIds.agent.parse(agentId);
1315
+ return id.ok
1316
+ ? (this.directory.read(this.grant(), id.value)?.createdBy ?? null)
1317
+ : null;
1318
+ }
1194
1319
  /** Owner-only rename; refuses a name already held by another Agent. */
1195
1320
  rename(agentId, name, principal) {
1196
1321
  if (!oysterPrincipalIsOwner(principal))
@@ -1453,7 +1578,19 @@ export function standaloneAgents(env, scope) {
1453
1578
  sessionOperations: everyOperation,
1454
1579
  });
1455
1580
  return {
1456
- list: async () => (await directory().list()).map(toAgent),
1581
+ list: async (options) => (await directory().list(options)).map(toAgent),
1582
+ setArchived: async (agentId, archived, principal) => {
1583
+ if (!oysterPrincipalIsOwner(principal))
1584
+ return { error: "not_granted" };
1585
+ if (!(await directory().has(agentId)))
1586
+ return { error: "not_found" };
1587
+ const agent = env.AGENTS.getByName(agentId);
1588
+ const pin = await agent.policyDirectory();
1589
+ if (pin?.directoryName !== directoryName ||
1590
+ pin.tenantId !== (scope?.tenantId ?? defaultTenant))
1591
+ return { error: "not_found" };
1592
+ return agent.setArchived(archived);
1593
+ },
1457
1594
  create: async (name, requestId, principal, actor, avatar) => {
1458
1595
  const entry = await directory().create(name, requestId, principal, actor, avatar);
1459
1596
  await env.AGENTS.getByName(entry.agentId).usePolicyDirectory({
@@ -1624,10 +1761,58 @@ export const agentFrameMethods = {
1624
1761
  },
1625
1762
  },
1626
1763
  AGENTS: {
1764
+ schedules: {
1765
+ decodeArgs: (raw) => agentScheduleBindingArgs.schedules.parse(raw),
1766
+ decodeResult: Schema.decodeUnknownSync(agentSchedulePageSchema, {
1767
+ onExcessProperty: "error",
1768
+ }),
1769
+ requiredOperations: () => ["schedules"],
1770
+ requestedResource: (raw) => ({
1771
+ kind: "agent",
1772
+ id: agentScheduleBindingArgs.schedules.parse(raw)[0].agentId,
1773
+ }),
1774
+ },
1775
+ pauseSchedule: {
1776
+ decodeArgs: (raw) => agentScheduleBindingArgs.pauseSchedule.parse(raw),
1777
+ decodeResult: Schema.decodeUnknownSync(agentScheduleControlReceiptSchema, {
1778
+ onExcessProperty: "error",
1779
+ }),
1780
+ requiredOperations: () => ["pauseSchedule"],
1781
+ requestedResource: (raw) => ({
1782
+ kind: "agent",
1783
+ id: agentScheduleBindingArgs.pauseSchedule.parse(raw)[0].agentId,
1784
+ }),
1785
+ },
1786
+ resumeSchedule: {
1787
+ decodeArgs: (raw) => agentScheduleBindingArgs.resumeSchedule.parse(raw),
1788
+ decodeResult: Schema.decodeUnknownSync(agentScheduleControlReceiptSchema, {
1789
+ onExcessProperty: "error",
1790
+ }),
1791
+ requiredOperations: () => ["resumeSchedule"],
1792
+ requestedResource: (raw) => ({
1793
+ kind: "agent",
1794
+ id: agentScheduleBindingArgs.resumeSchedule.parse(raw)[0].agentId,
1795
+ }),
1796
+ },
1797
+ deleteSchedule: {
1798
+ decodeArgs: (raw) => agentScheduleBindingArgs.deleteSchedule.parse(raw),
1799
+ decodeResult: Schema.decodeUnknownSync(agentScheduleControlReceiptSchema, {
1800
+ onExcessProperty: "error",
1801
+ }),
1802
+ requiredOperations: () => ["deleteSchedule"],
1803
+ requestedResource: (raw) => ({
1804
+ kind: "agent",
1805
+ id: agentScheduleBindingArgs.deleteSchedule.parse(raw)[0].agentId,
1806
+ }),
1807
+ },
1627
1808
  send: {
1628
1809
  decodeArgs: (raw) => agentBindingArgs.AGENTS.send.parse(raw),
1629
1810
  decodeResult: decodeAgentAccepted,
1630
1811
  requiredOperations: () => ["send"],
1812
+ requestedResource: (raw) => ({
1813
+ kind: "agent",
1814
+ id: agentBindingArgs.AGENTS.send.parse(raw)[0].agentId,
1815
+ }),
1631
1816
  },
1632
1817
  },
1633
1818
  };
@@ -1669,9 +1854,45 @@ export async function dispatchAgentBinding(env, scope, frame, props, method, arg
1669
1854
  });
1670
1855
  result = { status: accepted.status, commandId: accepted.commandId };
1671
1856
  }
1857
+ else if (props.binding === "AGENTS" &&
1858
+ (method === "schedules" ||
1859
+ method === "pauseSchedule" ||
1860
+ method === "resumeSchedule" ||
1861
+ method === "deleteSchedule")) {
1862
+ const [input] = agentScheduleBindingArgs[method].parse(args);
1863
+ if (!(await directory.has(input.agentId)))
1864
+ throw new Error("No such agent");
1865
+ const home = await directory.agentHome(input.agentId);
1866
+ if (home?.kind === "machine")
1867
+ throw new MachineLinkRefused("actor_not_allowed");
1868
+ if (home === null)
1869
+ throw new AppInstanceError("revoked");
1870
+ frame.signal.throwIfAborted();
1871
+ const agent = env.AGENTS.getByName(input.agentId);
1872
+ const pin = {
1873
+ directoryName: scope.durableName("agents"),
1874
+ tenantId: scope.tenantId,
1875
+ };
1876
+ if (method === "schedules") {
1877
+ result = await agent.readSchedules(pin, agentScheduleBindingArgs.schedules.parse(args)[0]);
1878
+ }
1879
+ else {
1880
+ result = await agent.controlSchedule(pin, agentScheduleBindingArgs[method].parse(args)[0], method === "pauseSchedule"
1881
+ ? "pause"
1882
+ : method === "resumeSchedule"
1883
+ ? "resume"
1884
+ : "delete");
1885
+ }
1886
+ }
1672
1887
  else {
1673
1888
  throw new AppInstanceError("revoked");
1674
1889
  }
1675
1890
  frame.signal.throwIfAborted();
1676
- return Schema.decodeUnknownSync(Schema.Union([inboxPageSchema, inboxVisitSchema, AcceptedSchema]), { onExcessProperty: "error" })(result);
1891
+ return Schema.decodeUnknownSync(Schema.Union([
1892
+ inboxPageSchema,
1893
+ inboxVisitSchema,
1894
+ AcceptedSchema,
1895
+ agentSchedulePageSchema,
1896
+ agentScheduleControlReceiptSchema,
1897
+ ]), { onExcessProperty: "error" })(result);
1677
1898
  }
@@ -9,7 +9,7 @@ import { z } from "zod";
9
9
  import { buildStartFailureReply } from "./builds.js";
10
10
  import * as Schema from "effect/Schema";
11
11
  import { AppInstanceError } from "../index.js";
12
- import { installOrSelect, verifyRelease, } from "../host.js";
12
+ import { installOrSelect, verifyRelease } from "../host.js";
13
13
  import { repositoryWorkflowSourceSchema } from "@fungi.computer/workflows-wire";
14
14
  import { installationIdSchema } from "../apps/index.js";
15
15
  import { appIdSchema, releaseSchema } from "@fungi.computer/puffball/effect";
@@ -9,8 +9,7 @@ import { publishSource } from "../pack.js";
9
9
  import { appBuildRecipe, readBuildManifest } from "../build/workspace-build.js";
10
10
  import { parseSourceRevision } from "@fungi.computer/puffball";
11
11
  import { releaseSchema } from "@fungi.computer/puffball/effect";
12
- import { grantCommandSchema, installationSchema, } from "../apps/installation.js";
13
- import { appPermissionPageSchema } from "../apps/permissions.js";
12
+ import { approveChosenDashboard } from "./dashboard-approval.js";
14
13
  import { installOnOyster, InstallationObservationError, } from "../installation-client.js";
15
14
  export const standaloneAppSet = [
16
15
  {
@@ -141,57 +140,6 @@ export async function installStandaloneAppSet(options) {
141
140
  init.signal = signal;
142
141
  return fetch(new URL(path, options.url), init);
143
142
  };
144
- const approveChosenDashboard = async (installed, generation) => {
145
- const name = installed.installationId;
146
- const path = `/api/installations/${encodeURIComponent(name)}/grants`;
147
- const projectionResponse = await owner("GET", path);
148
- if (!projectionResponse.ok)
149
- throw new Error(`${name} approval read failed (${String(projectionResponse.status)})`);
150
- const projection = appPermissionPageSchema.parse(await projectionResponse.json());
151
- const { permissions } = projection;
152
- const digest = installed.release.artifact.artifactDigest;
153
- if (projection.storeGeneration !== generation ||
154
- projection.installationId !== name ||
155
- projection.artifactDigest !== digest ||
156
- permissions.declaredOperations.artifactDigest !== digest ||
157
- permissions.authority.authorityVersion !==
158
- installed.authority.authorityVersion ||
159
- permissions.authority.declaredOperationsDigest !==
160
- installed.authority.declaredOperationsDigest ||
161
- permissions.declaredOperations.digest !==
162
- installed.authority.declaredOperationsDigest)
163
- throw new Error(`${name} approval snapshot changed`);
164
- const shell = permissions.declaredOperations.capabilities.find((row) => row.capability === "team.workbenchHost");
165
- if (shell === undefined)
166
- return;
167
- const command = grantCommandSchema.parse({
168
- kind: "change-grants",
169
- changes: [
170
- {
171
- capability: shell.capability,
172
- grant: { operations: shell.operations, resources: "tenant-scope" },
173
- },
174
- ],
175
- });
176
- const saved = await owner("PUT", path, Schema.decodeUnknownSync(Schema.Json)({
177
- storeGeneration: projection.storeGeneration,
178
- authorityVersion: installed.authority.authorityVersion,
179
- artifactDigest: digest,
180
- command,
181
- }));
182
- if (!saved.ok)
183
- throw new Error(`${name} approval failed (${String(saved.status)})`);
184
- const receipt = installationSchema.parse(await saved.json());
185
- if (receipt.installationId !== name ||
186
- receipt.instanceId !== installed.instanceId ||
187
- receipt.release.artifact.artifactDigest !== digest ||
188
- receipt.authority.authorityVersion !==
189
- installed.authority.authorityVersion + 1 ||
190
- !receipt.authority.approvedGrants.some((grant) => grant.capability === "team.workbenchHost" &&
191
- grant.resources === "tenant-scope" &&
192
- shell.operations.every((operation) => grant.operations.includes(operation))))
193
- throw new Error(`${name} approval incomplete`);
194
- };
195
143
  const idle = options.idle ??
196
144
  ((ms, signal) => sleep(ms, undefined, { signal }));
197
145
  /** Start one build and wait for its terminal outcome within the build
@@ -392,7 +340,7 @@ export async function installStandaloneAppSet(options) {
392
340
  if (record.status !== "enabled")
393
341
  throw new Error(`${name} fresh installation is not enabled`);
394
342
  if (next.app.dashboard === true)
395
- await approveChosenDashboard(record, operation.reference.generation);
343
+ await approveChosenDashboard(operation, owner);
396
344
  }
397
345
  options.report(`${operation.creation === "replacement" ? "Replaced" : "Installed"} ${name} from its build ${build.jobId}`);
398
346
  // Report-only UI check: never blocks, but the owner sees it here.
@@ -1,4 +1,4 @@
1
- import { Hono } from "hono";
1
+ import { type Routes } from "../authorization.js";
2
2
  import type { TenantScope } from "../tenant.js";
3
3
  import type { OysterBindingResult } from "../workers.js";
4
4
  import type { OysterEnv } from "../host.js";
@@ -34,8 +34,7 @@ export declare const appSourceFrameMethods: {
34
34
  * re-checked here (never trusted from a stale captured frame). */
35
35
  export declare function dispatchAppSourceBinding(env: AppSourceBindingEnv, scope: TenantScope, frame: InvocationFrame, method: string, args: readonly unknown[]): Promise<OysterBindingResult>;
36
36
  /** Operator-only: seed one App's editable source template (`install-apps.mjs`).
37
- * Mounted under `host.ownerRoutes`, already owner-key-gated for every `/api/*`
38
- * path before this handler runs (`host.ts`'s `oysterPrincipalIsOwner` check).
37
+ * Each route declares the owner requirement (`authorization.ts`).
39
38
  * The operator `git push`es the projected source to
40
39
  * `/api/apps/:appId/source.git`, then claims that head here. */
41
- export declare function appSourceOwnerRoutes(env: AppSourceBindingEnv, scope: TenantScope): Hono<OysterEnv>;
40
+ export declare function appSourceOwnerRoutes(env: AppSourceBindingEnv, scope: TenantScope): Routes<OysterEnv>;
@@ -1,4 +1,4 @@
1
- import { Hono } from "hono";
1
+ import { owner, routes as routeTable } from "../authorization.js";
2
2
  import { bodyLimit } from "hono/body-limit";
3
3
  import * as Schema from "effect/Schema";
4
4
  import { AppInstanceError } from "../index.js";
@@ -32,18 +32,14 @@ export async function dispatchAppSourceBinding(env, scope, frame, method, args)
32
32
  return Schema.decodeUnknownSync(appForkResultSchema)(result);
33
33
  }
34
34
  /** Operator-only: seed one App's editable source template (`install-apps.mjs`).
35
- * Mounted under `host.ownerRoutes`, already owner-key-gated for every `/api/*`
36
- * path before this handler runs (`host.ts`'s `oysterPrincipalIsOwner` check).
35
+ * Each route declares the owner requirement (`authorization.ts`).
37
36
  * The operator `git push`es the projected source to
38
37
  * `/api/apps/:appId/source.git`, then claims that head here. */
39
38
  export function appSourceOwnerRoutes(env, scope) {
40
- const routes = new Hono();
39
+ const routes = routeTable();
41
40
  const registry = () => env.REGISTRY.getByName(scope.durableName("registry"));
42
- routes.on(["GET", "POST"], "/api/apps/:appId/source.git/:suffix{info/refs|git-upload-pack|git-receive-pack}", (context) => registry().exchangeTemplateSource(context.req.param("appId"), context.req.param("suffix"), context.req.raw));
43
- routes.put("/api/apps/:appId/source", bodyLimit({
44
- maxSize: TEMPLATE_SEED_CLAIM_MAX_BYTES,
45
- onError: (context) => context.json({ kind: "invalid" }, 413),
46
- }), async (context) => {
41
+ routes.on(["GET", "POST"], "/api/apps/:appId/source.git/:suffix{info/refs|git-upload-pack|git-receive-pack}", owner("direct-or-app"), (context) => registry().exchangeTemplateSource(context.req.param("appId"), context.req.param("suffix"), context.req.raw));
42
+ routes.put("/api/apps/:appId/source", owner("direct-or-app"), async (context) => {
47
43
  const claim = parseTemplateSeedClaim(await context.req.text());
48
44
  if (claim === null)
49
45
  return context.json({ kind: "invalid" }, 400);
@@ -54,6 +50,13 @@ export function appSourceOwnerRoutes(env, scope) {
54
50
  ? 409
55
51
  : 503;
56
52
  return Response.json(result, { status });
53
+ }, {
54
+ middleware: [
55
+ bodyLimit({
56
+ maxSize: TEMPLATE_SEED_CLAIM_MAX_BYTES,
57
+ onError: (context) => context.json({ kind: "invalid" }, 413),
58
+ }),
59
+ ],
57
60
  });
58
61
  return routes;
59
62
  }
@@ -1,18 +1,6 @@
1
- /**
2
- * Owner routes for App builds on the standalone's Computer. They run after
3
- * the host's owner guard; the build owner is the registry.
4
- *
5
- * Repository Apps register through APPS.register from their committed root
6
- * metadata. Publishing an App's source (PUT /api/apps/:appId/source) instead
7
- * makes its template the build source at the returned seed revision.
8
- *
9
- * - `POST /api/builds` starts a build of that source at one exact commit.
10
- * - `GET /api/builds/:jobId` reads it; a finished build carries its Release,
11
- * which the owner installs or selects through the installation routes.
12
- */
13
- import { Hono } from "hono";
14
1
  import { type OysterBuildStartReason } from "../app-builds.js";
15
2
  import { type OysterEnv } from "../host.js";
3
+ import { type Routes } from "../authorization.js";
16
4
  import type { StandaloneRegistry } from "./registry.js";
17
5
  /** Both HTTP and APPS expose the same closed preflight reason. */
18
6
  export declare function buildStartFailureReply(reason: OysterBuildStartReason | "unavailable"): {
@@ -24,4 +12,4 @@ export declare function buildStartFailureReply(reason: OysterBuildStartReason |
24
12
  };
25
13
  /** The build routes over the tenant registry that owns builds, resolved per
26
14
  * request so mounting the routes reaches no registry. */
27
- export declare function standaloneBuildRoutes(registryFor: () => DurableObjectStub<StandaloneRegistry>): Hono<OysterEnv>;
15
+ export declare function standaloneBuildRoutes(registryFor: () => Pick<StandaloneRegistry, "startBuild" | "readBuild" | "readBuildLog">): Routes<OysterEnv>;
@@ -7,13 +7,14 @@
7
7
  * makes its template the build source at the returned seed revision.
8
8
  *
9
9
  * - `POST /api/builds` starts a build of that source at one exact commit.
10
- * - `GET /api/builds/:jobId` reads it; a finished build carries its Release,
11
- * which the owner installs or selects through the installation routes.
10
+ * - `GET /api/builds/:jobId` reads it; `?log` reads its combined raw output.
11
+ * Both require the same owner admission. A finished build carries its
12
+ * Release, which the owner installs or selects through the installation routes.
12
13
  */
13
- import { Hono } from "hono";
14
14
  import * as Schema from "effect/Schema";
15
15
  import { oysterBuildStartMessages, } from "../app-builds.js";
16
- import { oysterPrincipalIsOwner } from "../host.js";
16
+ import {} from "../host.js";
17
+ import { owner as ownerRoute, routes as routeTable, } from "../authorization.js";
17
18
  import { appIdSchema, releaseSchema, sourceRevisionSchema, } from "@fungi.computer/puffball/effect";
18
19
  const buildInput = Schema.Struct({
19
20
  appId: appIdSchema,
@@ -38,14 +39,9 @@ export function buildStartFailureReply(reason) {
38
39
  /** The build routes over the tenant registry that owns builds, resolved per
39
40
  * request so mounting the routes reaches no registry. */
40
41
  export function standaloneBuildRoutes(registryFor) {
41
- const routes = new Hono();
42
- routes.use("/api/builds/*", async (context, next) => oysterPrincipalIsOwner(context.get("principal"))
43
- ? next()
44
- : Response.json({ error: "Owner only" }, { status: 403 }));
45
- routes.post("/api/builds", async (context) => {
46
- const principal = context.get("principal");
47
- if (!oysterPrincipalIsOwner(principal))
48
- return Response.json({ error: "Owner only" }, { status: 403 });
42
+ const routes = routeTable();
43
+ const owner = ownerRoute("direct-or-app");
44
+ routes.post("/api/builds", owner, async (context, { principal }) => {
49
45
  let input;
50
46
  try {
51
47
  input = Schema.decodeUnknownSync(buildInput, {
@@ -70,7 +66,13 @@ export function standaloneBuildRoutes(registryFor) {
70
66
  return Response.json(reply.body, { status: reply.status });
71
67
  }
72
68
  });
73
- routes.get("/api/builds/:jobId", async (context) => {
69
+ routes.get("/api/builds/:jobId", owner, async (context) => {
70
+ if (context.req.query("log") !== undefined) {
71
+ const log = await registryFor().readBuildLog(context.req.param("jobId"));
72
+ return log === null
73
+ ? Response.json({ error: "Not found" }, { status: 404 })
74
+ : Response.json(log);
75
+ }
74
76
  const build = await registryFor().readBuild(context.req.param("jobId"));
75
77
  return build === null
76
78
  ? Response.json({ error: "Not found" }, { status: 404 })