@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
package/dist/apps/gate.js CHANGED
@@ -59,9 +59,15 @@ export const gateRefusalSchema = z.lazy(() => z.strictObject({
59
59
  "approval_required",
60
60
  "requires_not_granted",
61
61
  "invalid_input",
62
- "visitor_read_only",
63
62
  ]),
64
63
  }));
64
+ /** Availability uses "any"; a selected target must match both its kind and identity. */
65
+ function grantsResource(resources, resource) {
66
+ return (resources === "tenant-scope" ||
67
+ (resource === "any"
68
+ ? resources.length > 0
69
+ : resources.some((ref) => ref.kind === resource.kind && ref.id === resource.id)));
70
+ }
65
71
  /** Intersect stored approvals with the current verified Release without widening resource selectors. */
66
72
  export function grantsFor(installation, declaration, snapshot, capability, resource = "any") {
67
73
  if (installation.status !== "enabled" ||
@@ -73,17 +79,14 @@ export function grantsFor(installation, declaration, snapshot, capability, resou
73
79
  const declared = snapshot.capabilities.find((row) => row.capability === capability)
74
80
  ?.operations ?? [];
75
81
  if (!approved?.capabilityApproved ||
76
- (approved.resources !== "tenant-scope" &&
77
- (resource === "any"
78
- ? approved.resources.length === 0
79
- : !approved.resources.some((ref) => ref.kind === resource.kind && ref.id === resource.id))))
82
+ !grantsResource(approved.resources, resource))
80
83
  return null;
81
84
  const operations = approved.operations.filter((operation) => declared.includes(operation));
82
85
  return { ...approved, operations };
83
86
  }
84
87
  /** Approved binding operations from an already verified declaration. Hosts still pin
85
88
  * the selection/version/artifact and compare running frame grants as a subset. */
86
- export function approvedBindingGrants(installation, declaration) {
89
+ export function approvedBindingGrants(installation, declaration, resource = "any") {
87
90
  if (installation.status !== "enabled")
88
91
  return [];
89
92
  const bindings = [];
@@ -96,8 +99,7 @@ export function approvedBindingGrants(installation, declaration) {
96
99
  const approved = installation.authority.approvedGrants.find((grant) => grant.capability === capability);
97
100
  return (declaration.capabilities.includes(capability) &&
98
101
  approved?.capabilityApproved &&
99
- (approved.resources === "tenant-scope" ||
100
- approved.resources.length > 0) &&
102
+ grantsResource(approved.resources, resource) &&
101
103
  approved.operations.includes(bindingOperationKey(binding.name, name)));
102
104
  }));
103
105
  });
@@ -124,10 +126,7 @@ export function scopedBindings(captured, method, ceiling = "write") {
124
126
  /** Principal ceiling for invocation and metadata capture. Captures must pass this
125
127
  * to scopedBindings; its default write ceiling does not encode caller access. */
126
128
  export function appInvocationCeiling(facts) {
127
- return facts.access?.level === "read" ||
128
- facts.principal.actor.kind === "visitor"
129
- ? "read"
130
- : "write";
129
+ return facts.access?.level === "read" ? "read" : "write";
131
130
  }
132
131
  function admittedBindings(facts, method) {
133
132
  return scopedBindings(approvedBindingGrants(facts.installation, facts.declaration), method, appInvocationCeiling(facts));
@@ -135,9 +134,7 @@ function admittedBindings(facts, method) {
135
134
  const refusal = (code) => ({ kind: "refused", code });
136
135
  function admissionRefusal(facts) {
137
136
  const { installation, principal } = facts;
138
- const visitor = principal.actor.kind === "visitor";
139
- if (installation.status !== "enabled" ||
140
- (visitor && installation.visibility !== "public"))
137
+ if (installation.status !== "enabled")
141
138
  return refusal("unavailable");
142
139
  if (facts.declaredOperations.digest !==
143
140
  installation.authority.declaredOperationsDigest ||
@@ -153,8 +150,7 @@ function admissionRefusal(facts) {
153
150
  return null;
154
151
  }
155
152
  function methodMetadata(facts, name) {
156
- const { installation, principal } = facts;
157
- const visitor = principal.actor.kind === "visitor";
153
+ const { installation } = facts;
158
154
  if (installation.instanceId === null)
159
155
  return refusal("unavailable");
160
156
  if (!Schema.is(appApiMethodNameSchema)(name) ||
@@ -166,8 +162,6 @@ function methodMetadata(facts, name) {
166
162
  return refusal("not_declared");
167
163
  if (method.effect === "external")
168
164
  return refusal("approval_required");
169
- if (visitor && method.effect !== "read")
170
- return refusal("visitor_read_only");
171
165
  if (method.effect === "write" && facts.access?.level === "read")
172
166
  return refusal("level_too_low");
173
167
  return { kind: "admitted", method };
@@ -199,16 +193,13 @@ export function admits(facts, rawRequest) {
199
193
  if (!parsed.success)
200
194
  return refusal("invalid_input");
201
195
  const request = parsed.data;
202
- const { installation, principal } = facts;
203
- const visitor = principal.actor.kind === "visitor";
196
+ const { installation } = facts;
204
197
  const denied = admissionRefusal(facts);
205
198
  if (denied !== null)
206
199
  return denied;
207
200
  switch (request.kind) {
208
201
  case "backend":
209
202
  // Opaque App requests have no verified method selector; they require write authority.
210
- if (visitor)
211
- return refusal("visitor_read_only");
212
203
  if (installation.instanceId === null)
213
204
  return refusal("unavailable");
214
205
  if (facts.access?.level === "read")
@@ -242,8 +233,6 @@ export function admits(facts, rawRequest) {
242
233
  };
243
234
  }
244
235
  case "capability": {
245
- if (visitor)
246
- return refusal("visitor_read_only");
247
236
  if (!facts.declaration.capabilities.includes(request.capability) ||
248
237
  (request.operation !== undefined &&
249
238
  facts.declaredOperations.capabilities
@@ -258,8 +247,6 @@ export function admits(facts, rawRequest) {
258
247
  return { kind: "admitted", grants: [] };
259
248
  }
260
249
  case "capability-route": {
261
- if (visitor)
262
- return refusal("visitor_read_only");
263
250
  if (request.effect === "write" && facts.access?.level === "read")
264
251
  return refusal("level_too_low");
265
252
  if (!request.operation.startsWith("route:") ||
@@ -273,8 +260,6 @@ export function admits(facts, rawRequest) {
273
260
  return { kind: "admitted", grants: [] };
274
261
  }
275
262
  case "binding": {
276
- if (visitor)
277
- return refusal("visitor_read_only");
278
263
  if (installation.instanceId === null)
279
264
  return refusal("unavailable");
280
265
  const operation = appBackendBindingOperation(request.binding, request.operation);
@@ -1,6 +1,6 @@
1
1
  /** Installation records and pure admission rules; tenant state and commits stay host-owned. */
2
2
  export { applyGrantCommand, appAuthoritySchema, grantCommandSchema, type AppAuthority, type GrantCommand, type GrantContext, type GrantRefusal, applyInstallationCommand, approvedGrantSchema, declaredOperationsSchema, installationCommandSchema, installationIdSchema, installationRefusalSchema, installationSchema, type Approval, type ApprovedGrant, type CapabilityOperationDescriptor, type DeclaredOperations, grantChangeSchema, type GrantChange, type Installation, type InstallationCommand, type InstallationContext, type InstallationReader, type InstallationRefusal, type InstallationUpdate, type ResourceRef, } from "./installation.js";
3
- export { permissionPinsSchema, permissionSaveSchema, type PermissionPins, appPermissionsWireSchema, appPermissionPageSchema, permissionScreen, draftOf, editPermissions, planGrantChange, rebaseDraft, type PermissionDraft, type PermissionEdit, type PermissionScreen, type AppPermissionsWire, grantRequestFor, pendingCapabilities, type GrantRequestDecision, type ResourceOption, } from "./permissions.js";
3
+ export { permissionPinsSchema, permissionSaveSchema, type PermissionPins, appPermissionsWireSchema, appPermissionPageSchema, permissionScreen, draftOf, editPermissions, planGrantChange, rebaseDraft, type PermissionDraft, type PermissionEdit, type PermissionScreen, type AppPermissionsWire, type AppPermissionsPort, type AppPermissionsViewData, grantRequestFor, pendingCapabilities, type GrantRequestDecision, type ResourceOption, } from "./permissions.js";
4
4
  export { admits, admitMethodMetadata, appInvocationCeiling, approvedBindingGrants, scopedBindings, grantsFor, gateRefusalSchema, gateRequestSchema, invocationGrantsSchema, type AppMethodDeclaration, type AppMethodMetadataDecision, type GateDecision, type GateFacts, type GateRequest, type HostPrincipal, type InvocationGrants, } from "./gate.js";
5
5
  export { deriveDeclaredOperations, appCallOperation } from "./operations.js";
6
6
  export { appToolPolicySchema, type AppToolAccess, type AppToolEntry, type AppToolPolicy, } from "./tools.js";
@@ -110,6 +110,8 @@ export type GrantContext = Readonly<{
110
110
  declaration: AppDeclaration;
111
111
  declaredOperations: DeclaredOperations;
112
112
  now: number;
113
+ /** Host-attested configured built-in Shell; never a manifest or command field. */
114
+ builtInShell?: boolean;
113
115
  }>;
114
116
  export type GrantRefusal = Readonly<{
115
117
  kind: "refused";
@@ -121,10 +123,6 @@ export declare function applyGrantCommand(current: AppAuthority | null, rawComma
121
123
  export declare const declaredOperationsSchema: z.ZodLazy<z.ZodObject<{
122
124
  artifactDigest: z.ZodPipe<z.ZodUnknown, z.ZodTransform<import("@fungi.computer/puffball").Sha256Digest, unknown>>;
123
125
  digest: z.ZodPipe<z.ZodUnknown, z.ZodTransform<import("@fungi.computer/puffball").Sha256Digest, unknown>>;
124
- anonymousMethods: z.ZodReadonly<z.ZodArray<z.ZodReadonly<z.ZodObject<{
125
- name: z.ZodPipe<z.ZodUnknown, z.ZodTransform<string, unknown>>;
126
- digest: z.ZodPipe<z.ZodUnknown, z.ZodTransform<import("@fungi.computer/puffball").Sha256Digest, unknown>>;
127
- }, z.core.$strict>>>>;
128
126
  capabilities: z.ZodReadonly<z.ZodArray<z.ZodReadonly<z.ZodObject<{
129
127
  capability: z.ZodPipe<z.ZodUnknown, z.ZodTransform<"approvals" | "agent.list" | "agent.manage" | "agent.rename" | "agent.avatar" | "agent.toolPolicy" | "agent.read" | "agent.send" | "computer.list" | "computer.readFile" | "computer.writeFile" | "computer.execute" | "llm.generation" | "media.generation" | "games:servers" | "git.repository" | "apps.open" | "apps.call" | "app.storage" | "app.events.emit" | "team.workbenchHost" | "marketplace.browse" | "marketplace.purchase" | "workflow.run" | "workflow.publish" | "workflow.arm" | "world" | "channels.read" | "channels.personal.manage", unknown>>;
130
128
  operations: z.ZodLazy<z.ZodReadonly<z.ZodArray<z.ZodString>>>;
@@ -189,28 +187,6 @@ export declare const installationSchema: z.ZodObject<{
189
187
  disabled: "disabled";
190
188
  deleted: "deleted";
191
189
  }>;
192
- visibility: z.ZodEnum<{
193
- private: "private";
194
- public: "public";
195
- }>;
196
- approvedAnonymousMethods: z.ZodReadonly<z.ZodArray<z.ZodReadonly<z.ZodObject<{
197
- name: z.ZodPipe<z.ZodUnknown, z.ZodTransform<string, unknown>>;
198
- digest: z.ZodPipe<z.ZodUnknown, z.ZodTransform<import("@fungi.computer/puffball").Sha256Digest, unknown>>;
199
- }, z.core.$strict>>>>;
200
- anonymousApproval: z.ZodNullable<z.ZodLazy<z.ZodReadonly<z.ZodObject<{
201
- by: import("zod/mini").ZodMiniDiscriminatedUnion<[import("zod/mini").ZodMiniObject<{
202
- kind: import("zod/mini").ZodMiniLiteral<"member">;
203
- memberId: import("zod/mini").ZodMiniString<string>;
204
- }, z.core.$strip>, import("zod/mini").ZodMiniObject<{
205
- kind: import("zod/mini").ZodMiniLiteral<"agent">;
206
- agentId: import("zod/mini").ZodMiniString<string>;
207
- }, z.core.$strip>, import("zod/mini").ZodMiniObject<{
208
- kind: import("zod/mini").ZodMiniLiteral<"team-key">;
209
- keyId: import("zod/mini").ZodMiniString<string>;
210
- }, z.core.$strip>], "kind">;
211
- at: z.ZodNumber;
212
- authorityVersion: z.ZodLazy<z.ZodNumber>;
213
- }, z.core.$strict>>>>;
214
190
  }, z.core.$strict>;
215
191
  export type Installation = Readonly<z.output<typeof installationSchema>>;
216
192
  /** Host commands; floating targets have already been resolved to immutable Releases. */
@@ -259,12 +235,6 @@ export declare const installationCommandSchema: z.ZodLazy<z.ZodDiscriminatedUnio
259
235
  disabled: "disabled";
260
236
  deleted: "deleted";
261
237
  }>;
262
- }, z.core.$strict>, z.ZodObject<{
263
- kind: z.ZodLiteral<"set-visibility">;
264
- visibility: z.ZodEnum<{
265
- private: "private";
266
- public: "public";
267
- }>;
268
238
  }, z.core.$strict>, z.ZodObject<{
269
239
  kind: z.ZodLiteral<"change-grants">;
270
240
  changes: z.ZodReadonly<z.ZodArray<z.ZodReadonly<z.ZodObject<{
@@ -277,12 +247,6 @@ export declare const installationCommandSchema: z.ZodLazy<z.ZodDiscriminatedUnio
277
247
  }, z.core.$strict>>>>>]>;
278
248
  }, z.core.$strict>>>;
279
249
  }, z.core.$strict>>>>;
280
- }, z.core.$strict>, z.ZodObject<{
281
- kind: z.ZodLiteral<"approve-anonymous">;
282
- methods: z.ZodReadonly<z.ZodArray<z.ZodReadonly<z.ZodObject<{
283
- name: z.ZodPipe<z.ZodUnknown, z.ZodTransform<string, unknown>>;
284
- digest: z.ZodPipe<z.ZodUnknown, z.ZodTransform<import("@fungi.computer/puffball").Sha256Digest, unknown>>;
285
- }, z.core.$strict>>>>;
286
250
  }, z.core.$strict>], "kind">>;
287
251
  export type InstallationCommand = z.output<typeof installationCommandSchema>;
288
252
  /** Values captured by the host; identities and time are never generated by the reducer. */
@@ -294,10 +258,8 @@ export type InstallationContext = Readonly<{
294
258
  declaredOperations: DeclaredOperations;
295
259
  now: number;
296
260
  identity: Pick<Installation, "installationId" | "instanceId">;
297
- /** Trusted interactive consent; never copied from a command or inferred from a key's owner role. */
298
- anonymousConsent?: Readonly<{
299
- memberId: string;
300
- }>;
261
+ /** Operator-selected built-in Shell, attested by the executing host, not the App. */
262
+ builtInShell?: boolean;
301
263
  }>;
302
264
  /** Closed refusals: failed commands produce no installation update. */
303
265
  export declare const installationRefusalSchema: z.ZodLazy<z.ZodObject<{
@@ -1,7 +1,7 @@
1
1
  import * as z from "zod";
2
2
  import * as Schema from "effect/Schema";
3
3
  import * as Result from "effect/Result";
4
- import { appCapabilityIdSchema, appApiMethodNameSchema, } from "@fungi.computer/puffball/app";
4
+ import { appCapabilityIdSchema, } from "@fungi.computer/puffball/app";
5
5
  import { hostActorSchema } from "@fungi.computer/puffball/app-method-runtime";
6
6
  import { appIdSchema, releaseSchema, sha256DigestSchema, } from "@fungi.computer/puffball/effect";
7
7
  import { instanceIdSchema } from "../instance-id.js";
@@ -119,7 +119,6 @@ export function applyGrantCommand(current, rawCommand, context) {
119
119
  current.declaredOperationsDigest !== context.declaredOperations.digest))
120
120
  return refuse("stale_authority");
121
121
  if (context.principal.role === null ||
122
- context.principal.actor.kind === "visitor" ||
123
122
  (command.kind === "change-grants" && context.principal.role !== "owner"))
124
123
  return refuse("not_owner");
125
124
  const declared = (capability) => context.declaredOperations.capabilities.find((row) => row.capability === capability)?.operations ?? [];
@@ -131,7 +130,9 @@ export function applyGrantCommand(current, rawCommand, context) {
131
130
  grants =
132
131
  context.principal.role === "owner"
133
132
  ? context.declaration.capabilities
134
- .filter((capability) => permissionCatalog[capability].consent === "install")
133
+ .filter((capability) => permissionCatalog[capability].consent === "install" ||
134
+ (capability === "team.workbenchHost" &&
135
+ context.builtInShell === true))
135
136
  .map((capability) => ({
136
137
  capability,
137
138
  capabilityApproved: true,
@@ -190,23 +191,10 @@ export function applyGrantCommand(current, rawCommand, context) {
190
191
  approval,
191
192
  });
192
193
  }
193
- const anonymousMethodsSchema = z
194
- .array(z
195
- .strictObject({
196
- name: ownerCodec(appApiMethodNameSchema),
197
- digest: ownerCodec(sha256DigestSchema),
198
- })
199
- .readonly())
200
- .refine((methods) => methods.every((method, index) => {
201
- const previous = methods[index - 1];
202
- return previous === undefined || previous.name < method.name;
203
- }), "Anonymous method identities must have unique, sorted names")
204
- .readonly();
205
194
  /** Immutable operation facts derived from descriptor methods and the binding registry. */
206
195
  export const declaredOperationsSchema = z.lazy(() => z.strictObject({
207
196
  artifactDigest: ownerCodec(sha256DigestSchema),
208
197
  digest: ownerCodec(sha256DigestSchema),
209
- anonymousMethods: anonymousMethodsSchema,
210
198
  capabilities: z
211
199
  .array(z
212
200
  .strictObject({
@@ -226,20 +214,8 @@ export const installationSchema = z
226
214
  releasePin: z.enum(["pinned", "floating"]),
227
215
  authority: appAuthoritySchema,
228
216
  status: z.enum(["enabled", "disabled", "deleted"]),
229
- visibility: z.enum(["private", "public"]),
230
- approvedAnonymousMethods: anonymousMethodsSchema,
231
- anonymousApproval: approvalSchema.nullable(),
232
217
  })
233
- .refine((installation) => installation.appId === installation.release.appId, "Release must belong to the installation's App")
234
- .refine((installation) => {
235
- const approval = installation.anonymousApproval;
236
- return approval === null
237
- ? installation.approvedAnonymousMethods.length === 0
238
- : installation.status !== "deleted" &&
239
- installation.approvedAnonymousMethods.length > 0 &&
240
- approval.by.kind === "member" &&
241
- approval.authorityVersion <= installation.authority.authorityVersion;
242
- }, "Anonymous authority requires a retained human approval receipt");
218
+ .refine((installation) => installation.appId === installation.release.appId, "Release must belong to the installation's App");
243
219
  const releaseFields = {
244
220
  release: ownerCodec(releaseSchema),
245
221
  releasePin: installationSchema.shape.releasePin,
@@ -255,18 +231,10 @@ export const installationCommandSchema = z.lazy(() => z.discriminatedUnion("kind
255
231
  kind: z.literal("set-status"),
256
232
  status: installationSchema.shape.status,
257
233
  }),
258
- z.strictObject({
259
- kind: z.literal("set-visibility"),
260
- visibility: installationSchema.shape.visibility,
261
- }),
262
234
  z.strictObject({
263
235
  kind: z.literal("change-grants"),
264
236
  changes: grantChangesSchema,
265
237
  }),
266
- z.strictObject({
267
- kind: z.literal("approve-anonymous"),
268
- methods: anonymousMethodsSchema,
269
- }),
270
238
  ]));
271
239
  /** Closed refusals: failed commands produce no installation update. */
272
240
  export const installationRefusalSchema = z.lazy(() => z.strictObject({
@@ -301,13 +269,6 @@ export function applyInstallationCommand(current, rawCommand, context) {
301
269
  }
302
270
  if (command.kind === "change-grants" && context.role !== "owner")
303
271
  return refuse("not_owner");
304
- const anonymousConsent = context.by.kind === "member" &&
305
- context.anonymousConsent?.memberId === context.by.memberId;
306
- if (command.kind === "approve-anonymous") {
307
- const narrows = command.methods.every((method) => current?.approvedAnonymousMethods.some((approved) => approved.name === method.name && approved.digest === method.digest));
308
- if (context.role !== "owner" || (!narrows && !anonymousConsent))
309
- return refuse("not_owner");
310
- }
311
272
  const release = command.kind === "install" || command.kind === "select-release"
312
273
  ? command.release
313
274
  : current?.release;
@@ -327,7 +288,7 @@ export function applyInstallationCommand(current, rawCommand, context) {
327
288
  ? { kind: command.kind }
328
289
  : command.kind === "change-grants"
329
290
  ? command
330
- : command.kind === "set-status" && command.status === "deleted"
291
+ : command.status === "deleted"
331
292
  ? { kind: "delete" }
332
293
  : null;
333
294
  const authority = grantCommand === null
@@ -348,34 +309,10 @@ export function applyInstallationCommand(current, rawCommand, context) {
348
309
  declaration: context.declaration,
349
310
  declaredOperations: context.declaredOperations,
350
311
  now: context.now,
312
+ builtInShell: context.builtInShell === true,
351
313
  });
352
314
  if (Result.isFailure(authority))
353
315
  return refuse(authority.failure.code);
354
- let anonymousMethods = current?.approvedAnonymousMethods ?? [];
355
- let anonymousApproval = current?.anonymousApproval ?? null;
356
- if (command.kind === "approve-anonymous") {
357
- if (command.methods.some((method) => !context.declaredOperations.anonymousMethods.some((declared) => declared.name === method.name &&
358
- declared.digest === method.digest)))
359
- return refuse("not_declared");
360
- anonymousMethods = command.methods;
361
- if (command.methods.length === 0)
362
- anonymousApproval = null;
363
- else if (anonymousConsent)
364
- anonymousApproval = {
365
- by: context.by,
366
- at: context.now,
367
- authorityVersion: version,
368
- };
369
- }
370
- if (command.kind === "select-release") {
371
- anonymousMethods = anonymousMethods.filter((method) => context.declaredOperations.anonymousMethods.some((declared) => declared.name === method.name && declared.digest === method.digest));
372
- if (anonymousMethods.length === 0)
373
- anonymousApproval = null;
374
- }
375
- if (command.kind === "set-status" && command.status === "deleted") {
376
- anonymousMethods = [];
377
- anonymousApproval = null;
378
- }
379
316
  const next = installationSchema.safeParse({
380
317
  installationId: current?.installationId ?? context.identity.installationId,
381
318
  instanceId: current === null ? context.identity.instanceId : current.instanceId,
@@ -387,12 +324,7 @@ export function applyInstallationCommand(current, rawCommand, context) {
387
324
  status: command.kind === "set-status"
388
325
  ? command.status
389
326
  : (current?.status ?? "enabled"),
390
- visibility: command.kind === "set-visibility"
391
- ? command.visibility
392
- : (current?.visibility ?? "private"),
393
327
  authority: authority.success,
394
- approvedAnonymousMethods: anonymousMethods,
395
- anonymousApproval,
396
328
  });
397
329
  return next.success
398
330
  ? Result.succeed({ next: next.data, authorityChanged: true })
@@ -1,6 +1,5 @@
1
1
  import { appBackendBindingOperation, } from "@fungi.computer/puffball/app";
2
2
  import { sha256 } from "../release.js";
3
- import canonicalJson from "fast-json-stable-stringify";
4
3
  import { declaredOperationsSchema, } from "./installation.js";
5
4
  /** Qualify backend operations so a same-named browser method grants no backend authority. */
6
5
  export function bindingOperationKey(binding, operation) {
@@ -75,24 +74,15 @@ export async function deriveDeclaredOperations(release, declaration, descriptors
75
74
  operations: [...operations].sort(),
76
75
  }));
77
76
  const artifactDigest = release.artifact.artifactDigest;
78
- const anonymousMethods = await Promise.all(Object.entries(declaration.api?.methods ?? {})
79
- .filter(([, method]) => method.anonymous === true)
80
- .sort(([a], [b]) => (a < b ? -1 : a > b ? 1 : 0))
81
- .map(async ([name, method]) => ({
82
- name,
83
- digest: await sha256(new TextEncoder().encode(canonicalJson(method))),
84
- })));
85
77
  const digest = await sha256(new TextEncoder().encode(JSON.stringify({
86
78
  artifactDigest,
87
79
  capabilities: rows,
88
80
  routes,
89
81
  bindings,
90
- anonymousMethods,
91
82
  })));
92
83
  return declaredOperationsSchema.parse({
93
84
  artifactDigest,
94
85
  digest,
95
86
  capabilities: rows,
96
- anonymousMethods,
97
87
  });
98
88
  }
@@ -103,10 +103,6 @@ export declare const appPermissionsWireSchema: z.ZodObject<{
103
103
  declaredOperations: z.ZodLazy<z.ZodObject<{
104
104
  artifactDigest: z.ZodPipe<z.ZodUnknown, z.ZodTransform<import("@fungi.computer/puffball").Sha256Digest, unknown>>;
105
105
  digest: z.ZodPipe<z.ZodUnknown, z.ZodTransform<import("@fungi.computer/puffball").Sha256Digest, unknown>>;
106
- anonymousMethods: z.ZodReadonly<z.ZodArray<z.ZodReadonly<z.ZodObject<{
107
- name: z.ZodPipe<z.ZodUnknown, z.ZodTransform<string, unknown>>;
108
- digest: z.ZodPipe<z.ZodUnknown, z.ZodTransform<import("@fungi.computer/puffball").Sha256Digest, unknown>>;
109
- }, z.core.$strict>>>>;
110
106
  capabilities: z.ZodReadonly<z.ZodArray<z.ZodReadonly<z.ZodObject<{
111
107
  capability: z.ZodPipe<z.ZodUnknown, z.ZodTransform<"approvals" | "agent.list" | "agent.manage" | "agent.rename" | "agent.avatar" | "agent.toolPolicy" | "agent.read" | "agent.send" | "computer.list" | "computer.readFile" | "computer.writeFile" | "computer.execute" | "llm.generation" | "media.generation" | "games:servers" | "git.repository" | "apps.open" | "apps.call" | "app.storage" | "app.events.emit" | "team.workbenchHost" | "marketplace.browse" | "marketplace.purchase" | "workflow.run" | "workflow.publish" | "workflow.arm" | "world" | "channels.read" | "channels.personal.manage", unknown>>;
112
108
  operations: z.ZodLazy<z.ZodReadonly<z.ZodArray<z.ZodString>>>;
@@ -234,14 +230,28 @@ export declare const appPermissionPageSchema: z.ZodObject<{
234
230
  declaredOperations: z.ZodLazy<z.ZodObject<{
235
231
  artifactDigest: z.ZodPipe<z.ZodUnknown, z.ZodTransform<import("@fungi.computer/puffball").Sha256Digest, unknown>>;
236
232
  digest: z.ZodPipe<z.ZodUnknown, z.ZodTransform<import("@fungi.computer/puffball").Sha256Digest, unknown>>;
237
- anonymousMethods: z.ZodReadonly<z.ZodArray<z.ZodReadonly<z.ZodObject<{
238
- name: z.ZodPipe<z.ZodUnknown, z.ZodTransform<string, unknown>>;
239
- digest: z.ZodPipe<z.ZodUnknown, z.ZodTransform<import("@fungi.computer/puffball").Sha256Digest, unknown>>;
240
- }, z.core.$strict>>>>;
241
233
  capabilities: z.ZodReadonly<z.ZodArray<z.ZodReadonly<z.ZodObject<{
242
234
  capability: z.ZodPipe<z.ZodUnknown, z.ZodTransform<"approvals" | "agent.list" | "agent.manage" | "agent.rename" | "agent.avatar" | "agent.toolPolicy" | "agent.read" | "agent.send" | "computer.list" | "computer.readFile" | "computer.writeFile" | "computer.execute" | "llm.generation" | "media.generation" | "games:servers" | "git.repository" | "apps.open" | "apps.call" | "app.storage" | "app.events.emit" | "team.workbenchHost" | "marketplace.browse" | "marketplace.purchase" | "workflow.run" | "workflow.publish" | "workflow.arm" | "world" | "channels.read" | "channels.personal.manage", unknown>>;
243
235
  operations: z.ZodLazy<z.ZodReadonly<z.ZodArray<z.ZodString>>>;
244
236
  }, z.core.$strict>>>>;
245
237
  }, z.core.$strict>>;
246
238
  }, z.core.$strict>;
239
+ canEdit: z.ZodBoolean;
240
+ resources: z.ZodArray<z.ZodObject<{
241
+ kind: z.ZodString;
242
+ id: z.ZodString;
243
+ label: z.ZodString;
244
+ }, z.core.$strict>>;
247
245
  }, z.core.$strict>;
246
+ /** A host's live authority and resource directory, never browser-minted grants. */
247
+ export type AppPermissionsViewData = z.output<typeof appPermissionPageSchema>;
248
+ /**
249
+ * Hosts authenticate, fence and commit; the shared view owns the permission draft.
250
+ * @interface
251
+ */
252
+ export type AppPermissionsPort = Readonly<{
253
+ /** Read the installation's current permissions, whether the caller can edit them, and the available resources. */
254
+ read(appKey: string): Promise<AppPermissionsViewData | "unavailable">;
255
+ /** Submit edited grants with the store generation, authority version and artifact digest. The host checks authorization and freshness before saving. */
256
+ submit(appKey: string, pins: PermissionPins, changes: readonly GrantChange[]): Promise<"accepted" | "stale" | "refused" | "unavailable" | "unconfirmed">;
257
+ }>;
@@ -214,7 +214,7 @@ export function permissionScreen(input) {
214
214
  ? []
215
215
  : [
216
216
  row("all", scope === "computer" ? "All Computers" : "All Agents"),
217
- ...[...resources.values()].map((resource) => row(resource, resource.label)),
217
+ ...[...resources.values()].map((resource) => row({ kind: resource.kind, id: resource.id }, resource.label)),
218
218
  ],
219
219
  switches: capabilities
220
220
  .filter((capability) => permissionCatalog[capability].scope === null)
@@ -252,4 +252,10 @@ export const appPermissionPageSchema = z.strictObject({
252
252
  appName: z.string(),
253
253
  artifactDigest: ownerCodec(sha256DigestSchema),
254
254
  permissions: appPermissionsWireSchema,
255
+ canEdit: z.boolean(),
256
+ resources: z.array(z.strictObject({
257
+ kind: z.string(),
258
+ id: z.string(),
259
+ label: z.string(),
260
+ })),
255
261
  });
package/dist/auth.d.ts CHANGED
@@ -1,24 +1,7 @@
1
- import type { OysterAuth, OysterHost } from "./host.js";
1
+ import type { OysterAuth } from "./host.js";
2
2
  import type { OysterKeyStore } from "./keys.js";
3
3
  /** Cookie emitted by ownerKeyAuth, for composing credential-specific adapters. */
4
4
  export declare const ownerKeyCookieName = "oyster_session";
5
- /** Host-minted opaque sessions for named keys, separate from the owner cookie. */
6
- export declare const keySessionCookieName = "oyster_key_session";
7
- /**
8
- * The host owns session minting, hashing/storage and expiry.
9
- * A session resolves only to a key id, never to owner or grant authority.
10
- * Hosts should use Path=/, HttpOnly, Secure and SameSite=Lax (or stricter)
11
- * as defense in depth; Oyster independently checks request provenance.
12
- * Cookie-only admission allows GET/HEAD navigation. Every other method requires
13
- * Origin to be present and equal the request URL's origin, and Sec-Fetch-Site
14
- * not to be cross-site. Missing Origin is refused.
15
- */
16
- export type OysterKeySessionResolver = Readonly<{
17
- /** Resolve an opaque host session to its named key, or null when unavailable. */
18
- resolve: (value: string) => Promise<Readonly<{
19
- keyId: string;
20
- }> | null>;
21
- }>;
22
5
  /**
23
6
  * Single-owner sign-in: a bearer token or a session cookie holding the
24
7
  * owner key's digest. The key itself is never stored in the browser.
@@ -26,9 +9,3 @@ export type OysterKeySessionResolver = Readonly<{
26
9
  export declare function ownerKeyAuth(ownerKey: string, ownerId?: string): OysterAuth;
27
10
  /** The owner key, or a named key admitted as its canonical Team-key actor. */
28
11
  export declare function ownerOrKeyAuth(ownerKey: string, ownerId: string, keys: () => Pick<OysterKeyStore, "verify">): OysterAuth;
29
- /**
30
- * Add host-owned browser sessions to ownerOrKeyAuth. Explicit credentials and
31
- * non-401 refusals keep their ordinary result; ambient cookies cannot undo them.
32
- * Every cookie admission rechecks the key's liveness and canonical owner.
33
- */
34
- export declare function keySessionAuth(auth: OysterAuth, ownerId: string, keys: () => NonNullable<OysterHost["keys"]>, sessions: OysterKeySessionResolver): OysterAuth;
package/dist/auth.js CHANGED
@@ -1,13 +1,9 @@
1
1
  import * as Option from "effect/Option";
2
2
  import * as Schema from "effect/Schema";
3
- import { parse } from "hono/utils/cookie";
4
3
  import { presentedGitKey } from "./git-auth.js";
5
4
  import { sha256 } from "./release.js";
6
- import { sameOriginRequest } from "./request-origin.js";
7
5
  /** Cookie emitted by ownerKeyAuth, for composing credential-specific adapters. */
8
6
  export const ownerKeyCookieName = "oyster_session";
9
- /** Host-minted opaque sessions for named keys, separate from the owner cookie. */
10
- export const keySessionCookieName = "oyster_key_session";
11
7
  const loginFormSchema = Schema.Struct({ key: Schema.String });
12
8
  /** Compare two secrets through their fixed-length digests, in constant time. */
13
9
  async function constantTimeEqual(a, b) {
@@ -52,7 +48,15 @@ export function ownerKeyAuth(ownerKey, ownerId = "owner") {
52
48
  return { kind: "refused", status: 401 };
53
49
  },
54
50
  async login(request) {
55
- const form = Schema.decodeUnknownOption(loginFormSchema)(Object.fromEntries(await request.formData()));
51
+ // A body that is not a form is a malformed sign-in, not a server fault.
52
+ const fields = await request.formData().catch((cause) => {
53
+ if (cause instanceof TypeError)
54
+ return null;
55
+ throw cause;
56
+ });
57
+ if (fields === null)
58
+ return new Response("Expected a sign-in form", { status: 400 });
59
+ const form = Schema.decodeUnknownOption(loginFormSchema)(Object.fromEntries(fields));
56
60
  if (Option.isNone(form) ||
57
61
  !(await constantTimeEqual(form.value.key, ownerKey)))
58
62
  return new Response("Wrong key", { status: 401 });
@@ -95,37 +99,3 @@ export function ownerOrKeyAuth(ownerKey, ownerId, keys) {
95
99
  Promise.resolve(new Response("Sign-in is not available", { status: 404 })),
96
100
  };
97
101
  }
98
- /**
99
- * Add host-owned browser sessions to ownerOrKeyAuth. Explicit credentials and
100
- * non-401 refusals keep their ordinary result; ambient cookies cannot undo them.
101
- * Every cookie admission rechecks the key's liveness and canonical owner.
102
- */
103
- export function keySessionAuth(auth, ownerId, keys, sessions) {
104
- return {
105
- ...auth,
106
- async authenticate(request) {
107
- const result = await auth.authenticate(request);
108
- if (result.kind === "admitted" ||
109
- result.status !== 401 ||
110
- request.headers.has("Authorization"))
111
- return result;
112
- if (request.method !== "GET" &&
113
- request.method !== "HEAD" &&
114
- !sameOriginRequest(request))
115
- return result;
116
- const value = parse(request.headers.get("Cookie") ?? "", keySessionCookieName)[keySessionCookieName];
117
- if (value === undefined || value.length === 0)
118
- return result;
119
- const session = await sessions.resolve(value);
120
- if (session === null)
121
- return result;
122
- const key = await keys().live(session.keyId);
123
- return key !== null && key.owner === ownerId
124
- ? {
125
- kind: "admitted",
126
- principal: { actor: { kind: "team-key", keyId: key.keyId } },
127
- }
128
- : result;
129
- },
130
- };
131
- }