@fungi.computer/oyster 0.6.0 → 0.7.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 +132 -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 +50 -0
  5. package/dist/agent-schedules.js +17 -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 +59 -21
  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 +2 -0
  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 +153 -272
  47. package/dist/installation-execution.js +10 -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 +157 -20
  86. package/dist/standalone/agents.js +234 -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 +8 -3
  116. package/dist/standalone/workflow-binding.js +14 -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 +14 -5
  124. package/dist/standalone/workflow-schedules.js +11 -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 +36 -28
  128. package/dist/standalone/workflows.js +6 -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 +2 -0
  139. package/dist/workflows.js +1 -0
  140. package/docs/api/README.md +7 -0
  141. package/docs/api/agent-schedules.md +175 -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 +40 -37
  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 +124 -235
  159. package/package.json +39 -20
  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,17 @@ 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, agentScheduleTargetSchema, } from "@fungi.computer/shiitake-extensions/schedules";
41
44
  import { createAgentAvatarStore, } from "../agent-avatars.js";
42
45
  import { defineIdFamily } from "@fungi.computer/cairn";
43
46
  import { agentIds, createAgentDirectory, createFleetSessions, commandSettlementSchema, DirectoryError, } from "@fungi.computer/shiitake/fleet";
44
47
  import { appToolPolicySchema } from "../apps/index.js";
45
- import { oysterPrincipalIsOwner, oysterPrincipalId, } from "../host.js";
48
+ import { oysterPrincipalId } from "../host.js";
49
+ import { oysterPrincipalIsOwner } from "../authorization.js";
46
50
  import { defaultTenant, tenantIdSchema, tenantScope, } from "../tenant.js";
47
51
  import { createCodemodeSandbox } from "@fungi.computer/mycelium/workers";
52
+ import { forageDiscoveryInstructions } from "@fungi.computer/mycelium/effect";
48
53
  import { maxAgentMcpServers, } from "../agents.js";
49
54
  /** Own transport evidence and exact-message decisions for one model lease. */
50
55
  function observedModels(model, defaults) {
@@ -219,6 +224,7 @@ const toMcpServer = (stored) => ({
219
224
  hasBearer: stored.bearer !== null,
220
225
  });
221
226
  const instructions = [
227
+ forageDiscoveryInstructions,
222
228
  "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
229
  "You are an agent on a personal Oyster: a small, self-hosted home for Apps.",
224
230
  "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 +235,29 @@ const instructions = [
229
235
  ].join("\n");
230
236
  /** One agent: Shiitake's Cloudflare host over this Durable Object; its root supplies `hostFor`. */
231
237
  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;
238
+ archiveControlOwner;
239
+ archiveControl() {
240
+ return (this.archiveControlOwner ??= agentArchiveOwner({
241
+ storage: this.ctx.storage,
242
+ agentId: () => this.ctx.id.name,
243
+ agent: () => this.shiitake(),
244
+ scheduledExtensions: true,
245
+ refreshOutstanding: (agentId) => this.delegation().refreshOutstanding((target, sessionId, commandId) => this.machineClient().then((client) => client.settlement(target, sessionId, commandId, agentId))),
246
+ record: async (agentId, archived, revision) => {
247
+ const pin = await this.policyDirectory();
248
+ if (pin === null)
249
+ return false;
250
+ return this.env.AGENT_DIRECTORY.getByName(pin.directoryName).recordArchive(agentId, archived, revision);
251
+ },
252
+ wake: () => this.wakeRuntime(),
253
+ waitUntil: (task) => {
254
+ this.ctx.waitUntil(task);
255
+ },
256
+ }));
257
+ }
258
+ /** Owner-local atomic transition. The caller checks human/creator authority. */
259
+ setArchived(archived) {
260
+ return this.archiveControl().setArchived(archived);
248
261
  }
249
262
  /** Each installed Release's verified catalog surface, built once per Release for this Agent's lifetime. */
250
263
  #appSurfaces;
@@ -389,17 +402,40 @@ export class StandaloneAgentBase extends ShiitakeDO {
389
402
  }
390
403
  }
391
404
  controlDirectory() {
392
- const list = async (signal) => {
405
+ const list = async (signal, options) => {
393
406
  signal.throwIfAborted();
394
407
  const pin = await this.policyDirectory();
395
408
  if (pin === null)
396
409
  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))
410
+ const directory = this.env.AGENT_DIRECTORY.getByName(pin.directoryName);
411
+ const agents = await directory.list(options);
412
+ if (this.ctx.id.name === undefined ||
413
+ !(await directory.has(this.ctx.id.name)))
399
414
  throw new Error("Agent is outside this scope");
400
415
  signal.throwIfAborted();
401
416
  return agents;
402
417
  };
418
+ const changeArchive = async (targetId, archived, signal) => {
419
+ await list(signal);
420
+ const caller = this.ctx.id.name;
421
+ const pin = await this.policyDirectory();
422
+ if (caller === undefined || pin === null)
423
+ throw new AgentArchiveRefused("unreadable", "Agent scope is unavailable");
424
+ if (targetId === caller)
425
+ throw new AgentArchiveRefused("self", "An Agent cannot archive or restore itself");
426
+ const directory = this.env.AGENT_DIRECTORY.getByName(pin.directoryName);
427
+ if ((await directory.createdBy(targetId)) !== caller)
428
+ throw new AgentArchiveRefused("not_creator", "An Agent can change only one it created");
429
+ const target = this.env.AGENTS.getByName(targetId);
430
+ const targetPin = await target.policyDirectory();
431
+ if (targetPin?.directoryName !== pin.directoryName ||
432
+ targetPin.tenantId !== pin.tenantId)
433
+ throw new AgentArchiveRefused("unreadable", "Agent archive scope does not match");
434
+ signal.throwIfAborted();
435
+ const result = await target.setArchived(archived);
436
+ if ("error" in result)
437
+ throw new AgentArchiveRefused(result.error, result.message ?? `Agent archive refused: ${result.error}`);
438
+ };
403
439
  return {
404
440
  list,
405
441
  create: async (input, signal) => {
@@ -420,10 +456,17 @@ export class StandaloneAgentBase extends ShiitakeDO {
420
456
  },
421
457
  send: (input, sessionId, signal, invocation) => this.delegation().send(input, sessionId, signal, invocation),
422
458
  recall: (input, sessionId, signal) => this.delegation().recall(input, sessionId, signal),
459
+ archive: (input, signal) => changeArchive(input.agentId, true, signal),
460
+ unarchive: (input, signal) => changeArchive(input.agentId, false, signal),
423
461
  open: async (agentId, signal) => {
424
462
  const admit = async () => {
425
- if (!(await list(signal)).some((agent) => agent.agentId === agentId))
463
+ if (!(await list(signal)).some((agent) => agent.agentId === agentId)) {
464
+ const pin = await this.policyDirectory();
465
+ if (pin !== null &&
466
+ (await this.env.AGENT_DIRECTORY.getByName(pin.directoryName).archived(agentId)))
467
+ throw new ShiitakeError("contract", "archived", "Agent is archived");
426
468
  throw new Error("Agent is outside this scope");
469
+ }
427
470
  };
428
471
  await admit();
429
472
  const agent = createShiitakeClient({
@@ -454,7 +497,19 @@ export class StandaloneAgentBase extends ShiitakeDO {
454
497
  ? null
455
498
  : { scopeId: pin.directoryName, agentId };
456
499
  },
457
- directory: () => this.controlDirectory(),
500
+ // Delegation custody and settlement still see archived identities;
501
+ // Agent-control discovery keeps its active-only default. New sends
502
+ // reach the recipient's admission door and get contract/archived.
503
+ directory: () => {
504
+ const control = this.controlDirectory();
505
+ return {
506
+ ...control,
507
+ list: async (signal) => [
508
+ ...(await control.list(signal)),
509
+ ...(await control.list(signal, { archived: true })),
510
+ ],
511
+ };
512
+ },
458
513
  deliver: (mail) => this.mail().deliver(mail),
459
514
  prepare: () => Promise.resolve(),
460
515
  stageAcceptedNotice: (mail, scopeId) => {
@@ -632,6 +687,28 @@ export class StandaloneAgentBase extends ShiitakeDO {
632
687
  ? null
633
688
  : Schema.decodeUnknownSync(policyDirectorySchema)(stored);
634
689
  }
690
+ /** Borrow the native owner only after matching its real initialization pin. */
691
+ async scheduleOwner(expected, agentId) {
692
+ const directory = Schema.decodeUnknownSync(policyDirectorySchema)(expected);
693
+ const pin = await this.policyDirectory();
694
+ if (agentId !== this.ctx.id.name ||
695
+ pin?.directoryName !== directory.directoryName ||
696
+ pin.tenantId !== directory.tenantId)
697
+ throw new AppInstanceError("revoked");
698
+ return this.shiitake();
699
+ }
700
+ /** One admitted Agent's bounded native schedule page; this is not a directory scan. */
701
+ async readSchedules(directory, input) {
702
+ const { agentId, ...page } = appAgentScheduleListInputSchema.parse(input);
703
+ const owner = await this.scheduleOwner(directory, agentId);
704
+ return readAgentSchedulePage(agentId, owner, page);
705
+ }
706
+ /** Manage authority returns native identity/state only, never schedule content. */
707
+ async controlSchedule(directory, input, action) {
708
+ const target = agentScheduleTargetSchema.parse(input);
709
+ const owner = await this.scheduleOwner(directory, target.agentId);
710
+ return controlAgentSchedule(owner, target, action);
711
+ }
635
712
  /** Listeners from live codemode acquisitions, told when the server list changes. */
636
713
  mcpInvalidations = new Set();
637
714
  async storedMcpServers() {
@@ -697,7 +774,13 @@ export class StandaloneAgentBase extends ShiitakeDO {
697
774
  meta: { schema: FungiMessageMetaSchema },
698
775
  ...this.mail().host,
699
776
  instructions: {
700
- load: () => Promise.resolve({ revision: "oyster-5", text: instructions }),
777
+ load: async () => {
778
+ const { appApprovalInstructions } = await import("../mcp.js");
779
+ return {
780
+ revision: "oyster-7",
781
+ text: instructions + "\n" + appApprovalInstructions,
782
+ };
783
+ },
701
784
  },
702
785
  tools: {
703
786
  sources: [
@@ -777,7 +860,16 @@ export class StandaloneAgentBase extends ShiitakeDO {
777
860
  grants: [...extensions.grants, homeGrant],
778
861
  },
779
862
  ...keeping,
780
- hostWork: [...this.mail().host.hostWork, ...keeping.hostWork],
863
+ admission: {
864
+ assertOpen: () => {
865
+ this.archiveControl().assertOpen();
866
+ },
867
+ },
868
+ hostWork: [
869
+ ...this.mail().host.hostWork,
870
+ ...keeping.hostWork,
871
+ this.archiveControl().work,
872
+ ],
781
873
  };
782
874
  }
783
875
  /** Send an installation-admitted message through fleet Session operations. */
@@ -841,6 +933,27 @@ const visitRow = Schema.Struct({
841
933
  const counterRow = Schema.Struct({ value: Schema.Number });
842
934
  /** Names this Oyster's agents and remembers which Sessions the member opened. */
843
935
  export class AgentDirectory extends DurableObject {
936
+ archiveProjectionOwner;
937
+ archiveProjection() {
938
+ return (this.archiveProjectionOwner ??= agentArchiveProjection(this.ctx.storage.sql));
939
+ }
940
+ /** Apply the Agent DO's revisioned fact; an older repair cannot undo a newer one. */
941
+ recordArchive(agentId, archived, revision) {
942
+ const projection = this.archiveProjection();
943
+ return this.ctx.storage.transactionSync(() => {
944
+ if (!this.has(agentId))
945
+ return false;
946
+ const result = projection.record(agentId, archived, revision);
947
+ if (result === "advanced")
948
+ this.bump();
949
+ // A replay after projection but before the Agent's ack still succeeds.
950
+ return result !== "stale";
951
+ });
952
+ }
953
+ /** Directory's bounded-stale projection, never the Agent's flag authority. */
954
+ archived(agentId) {
955
+ return this.archiveProjection().archived(agentId);
956
+ }
844
957
  computers;
845
958
  computerNames = new Map();
846
959
  directory;
@@ -1172,10 +1285,14 @@ export class AgentDirectory extends DurableObject {
1172
1285
  provenance: { actor: oysterPrincipalId(owner), via: captured.agentId },
1173
1286
  };
1174
1287
  }
1175
- list() {
1288
+ list(options) {
1289
+ const archived = this.archiveProjection().archivedIds();
1176
1290
  return this.directory
1177
1291
  .list(this.grant())
1178
1292
  .toReversed()
1293
+ .filter(({ agentId }) => options?.archived === true
1294
+ ? archived.has(agentId)
1295
+ : !archived.has(agentId))
1179
1296
  .map(({ agentId, name, createdBy }) => ({
1180
1297
  agentId,
1181
1298
  createdBy,
@@ -1191,6 +1308,13 @@ export class AgentDirectory extends DurableObject {
1191
1308
  readCreator(agentId) {
1192
1309
  return this.directory.readCreator(this.grant(), agentId);
1193
1310
  }
1311
+ /** Immutable creating Agent, distinct from the human credential owner. */
1312
+ createdBy(agentId) {
1313
+ const id = agentIds.agent.parse(agentId);
1314
+ return id.ok
1315
+ ? (this.directory.read(this.grant(), id.value)?.createdBy ?? null)
1316
+ : null;
1317
+ }
1194
1318
  /** Owner-only rename; refuses a name already held by another Agent. */
1195
1319
  rename(agentId, name, principal) {
1196
1320
  if (!oysterPrincipalIsOwner(principal))
@@ -1453,7 +1577,19 @@ export function standaloneAgents(env, scope) {
1453
1577
  sessionOperations: everyOperation,
1454
1578
  });
1455
1579
  return {
1456
- list: async () => (await directory().list()).map(toAgent),
1580
+ list: async (options) => (await directory().list(options)).map(toAgent),
1581
+ setArchived: async (agentId, archived, principal) => {
1582
+ if (!oysterPrincipalIsOwner(principal))
1583
+ return { error: "not_granted" };
1584
+ if (!(await directory().has(agentId)))
1585
+ return { error: "not_found" };
1586
+ const agent = env.AGENTS.getByName(agentId);
1587
+ const pin = await agent.policyDirectory();
1588
+ if (pin?.directoryName !== directoryName ||
1589
+ pin.tenantId !== (scope?.tenantId ?? defaultTenant))
1590
+ return { error: "not_found" };
1591
+ return agent.setArchived(archived);
1592
+ },
1457
1593
  create: async (name, requestId, principal, actor, avatar) => {
1458
1594
  const entry = await directory().create(name, requestId, principal, actor, avatar);
1459
1595
  await env.AGENTS.getByName(entry.agentId).usePolicyDirectory({
@@ -1624,10 +1760,47 @@ export const agentFrameMethods = {
1624
1760
  },
1625
1761
  },
1626
1762
  AGENTS: {
1763
+ schedules: {
1764
+ decodeArgs: (raw) => agentScheduleBindingArgs.schedules.parse(raw),
1765
+ decodeResult: Schema.decodeUnknownSync(agentSchedulePageSchema, {
1766
+ onExcessProperty: "error",
1767
+ }),
1768
+ requiredOperations: () => ["schedules"],
1769
+ requestedResource: (raw) => ({
1770
+ kind: "agent",
1771
+ id: agentScheduleBindingArgs.schedules.parse(raw)[0].agentId,
1772
+ }),
1773
+ },
1774
+ pauseSchedule: {
1775
+ decodeArgs: (raw) => agentScheduleBindingArgs.pauseSchedule.parse(raw),
1776
+ decodeResult: Schema.decodeUnknownSync(agentScheduleControlReceiptSchema, {
1777
+ onExcessProperty: "error",
1778
+ }),
1779
+ requiredOperations: () => ["pauseSchedule"],
1780
+ requestedResource: (raw) => ({
1781
+ kind: "agent",
1782
+ id: agentScheduleBindingArgs.pauseSchedule.parse(raw)[0].agentId,
1783
+ }),
1784
+ },
1785
+ deleteSchedule: {
1786
+ decodeArgs: (raw) => agentScheduleBindingArgs.deleteSchedule.parse(raw),
1787
+ decodeResult: Schema.decodeUnknownSync(agentScheduleControlReceiptSchema, {
1788
+ onExcessProperty: "error",
1789
+ }),
1790
+ requiredOperations: () => ["deleteSchedule"],
1791
+ requestedResource: (raw) => ({
1792
+ kind: "agent",
1793
+ id: agentScheduleBindingArgs.deleteSchedule.parse(raw)[0].agentId,
1794
+ }),
1795
+ },
1627
1796
  send: {
1628
1797
  decodeArgs: (raw) => agentBindingArgs.AGENTS.send.parse(raw),
1629
1798
  decodeResult: decodeAgentAccepted,
1630
1799
  requiredOperations: () => ["send"],
1800
+ requestedResource: (raw) => ({
1801
+ kind: "agent",
1802
+ id: agentBindingArgs.AGENTS.send.parse(raw)[0].agentId,
1803
+ }),
1631
1804
  },
1632
1805
  },
1633
1806
  };
@@ -1669,9 +1842,40 @@ export async function dispatchAgentBinding(env, scope, frame, props, method, arg
1669
1842
  });
1670
1843
  result = { status: accepted.status, commandId: accepted.commandId };
1671
1844
  }
1845
+ else if (props.binding === "AGENTS" &&
1846
+ (method === "schedules" ||
1847
+ method === "pauseSchedule" ||
1848
+ method === "deleteSchedule")) {
1849
+ const [input] = agentScheduleBindingArgs[method].parse(args);
1850
+ if (!(await directory.has(input.agentId)))
1851
+ throw new Error("No such agent");
1852
+ const home = await directory.agentHome(input.agentId);
1853
+ if (home?.kind === "machine")
1854
+ throw new MachineLinkRefused("actor_not_allowed");
1855
+ if (home === null)
1856
+ throw new AppInstanceError("revoked");
1857
+ frame.signal.throwIfAborted();
1858
+ const agent = env.AGENTS.getByName(input.agentId);
1859
+ const pin = {
1860
+ directoryName: scope.durableName("agents"),
1861
+ tenantId: scope.tenantId,
1862
+ };
1863
+ if (method === "schedules") {
1864
+ result = await agent.readSchedules(pin, agentScheduleBindingArgs.schedules.parse(args)[0]);
1865
+ }
1866
+ else {
1867
+ result = await agent.controlSchedule(pin, agentScheduleBindingArgs[method].parse(args)[0], method === "pauseSchedule" ? "pause" : "delete");
1868
+ }
1869
+ }
1672
1870
  else {
1673
1871
  throw new AppInstanceError("revoked");
1674
1872
  }
1675
1873
  frame.signal.throwIfAborted();
1676
- return Schema.decodeUnknownSync(Schema.Union([inboxPageSchema, inboxVisitSchema, AcceptedSchema]), { onExcessProperty: "error" })(result);
1874
+ return Schema.decodeUnknownSync(Schema.Union([
1875
+ inboxPageSchema,
1876
+ inboxVisitSchema,
1877
+ AcceptedSchema,
1878
+ agentSchedulePageSchema,
1879
+ agentScheduleControlReceiptSchema,
1880
+ ]), { onExcessProperty: "error" })(result);
1677
1881
  }
@@ -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 })