@fungi.computer/oyster 0.2.0 → 0.3.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 (287) hide show
  1. package/README.md +376 -72
  2. package/dist/agents.d.ts +3 -2
  3. package/dist/agents.js +4 -2
  4. package/dist/app-builds.d.ts +81 -8
  5. package/dist/app-builds.js +210 -60
  6. package/dist/app-storage.d.ts +3 -3
  7. package/dist/approval-capability.d.ts +5 -0
  8. package/dist/approval-capability.js +46 -0
  9. package/dist/approval-home.d.ts +51 -0
  10. package/dist/approval-home.js +226 -0
  11. package/dist/apps/catalog.d.ts +335 -0
  12. package/dist/apps/catalog.js +46 -0
  13. package/dist/apps/gate.d.ts +20 -6
  14. package/dist/apps/gate.js +42 -16
  15. package/dist/apps/index.d.ts +3 -1
  16. package/dist/apps/index.js +3 -1
  17. package/dist/apps/installation.d.ts +132 -51
  18. package/dist/apps/installation.js +176 -70
  19. package/dist/apps/permissions.d.ts +247 -0
  20. package/dist/apps/permissions.js +255 -0
  21. package/dist/boot-capability-route.d.ts +15 -0
  22. package/dist/boot-capability-route.js +196 -0
  23. package/dist/boot-capability.d.ts +103 -0
  24. package/dist/boot-capability.js +141 -0
  25. package/dist/boot-construction.d.ts +12 -0
  26. package/dist/boot-construction.js +175 -0
  27. package/dist/boot-gate.d.ts +267 -0
  28. package/dist/boot-gate.js +236 -0
  29. package/dist/boot-host-script.d.ts +2 -0
  30. package/dist/boot-host-script.js +1 -0
  31. package/dist/boot-mount.d.ts +21 -0
  32. package/dist/boot-mount.js +65 -0
  33. package/dist/browser/boot.d.ts +73 -0
  34. package/dist/browser/boot.js +502 -0
  35. package/dist/browser/browser.d.ts +104 -0
  36. package/dist/browser/browser.js +374 -0
  37. package/dist/browser/capability-source.d.ts +76 -0
  38. package/dist/browser/capability-source.js +338 -0
  39. package/dist/browser/cdp-client.d.ts +56 -0
  40. package/dist/browser/cdp-client.js +184 -0
  41. package/dist/browser/errors.d.ts +79 -0
  42. package/dist/browser/errors.js +32 -0
  43. package/dist/browser/index.d.ts +16 -0
  44. package/dist/browser/index.js +14 -0
  45. package/dist/browser/lease-registry.d.ts +61 -0
  46. package/dist/browser/lease-registry.js +101 -0
  47. package/dist/browser/operations.d.ts +225 -0
  48. package/dist/browser/operations.js +137 -0
  49. package/dist/browser/page.d.ts +25 -0
  50. package/dist/browser/page.js +172 -0
  51. package/dist/browser/quick-reads.d.ts +29 -0
  52. package/dist/browser/quick-reads.js +160 -0
  53. package/dist/browser/sessions.d.ts +52 -0
  54. package/dist/browser/sessions.js +118 -0
  55. package/dist/browser/url-policy.d.ts +18 -0
  56. package/dist/browser/url-policy.js +58 -0
  57. package/dist/build/app-imports.d.ts +12 -0
  58. package/dist/build/app-imports.js +105 -0
  59. package/dist/build/build-policy.d.ts +5 -3
  60. package/dist/build/build-policy.js +5 -3
  61. package/dist/build/candidate-builder.d.ts +1 -1
  62. package/dist/build/candidate-cli.mjs +8375 -334
  63. package/dist/build/local-build.d.ts +2 -0
  64. package/dist/build/local-build.js +28 -6
  65. package/dist/build/snapshot-package-store.d.ts +1 -1
  66. package/dist/build/snapshot-package-store.js +7 -2
  67. package/dist/build/workspace-build.d.ts +92 -5
  68. package/dist/build/workspace-build.js +93 -19
  69. package/dist/build.d.ts +1 -1
  70. package/dist/build.js +1 -1
  71. package/dist/catalog-frame.js +1 -1
  72. package/dist/cloudflare.d.ts +13 -3
  73. package/dist/cloudflare.js +102 -4
  74. package/dist/frame-binding-wire-script.js +1 -1
  75. package/dist/frame-binding-wire.js +4 -2
  76. package/dist/git-auth.d.ts +0 -2
  77. package/dist/git-auth.js +0 -2
  78. package/dist/git.d.ts +0 -1
  79. package/dist/git.js +0 -2
  80. package/dist/host.d.ts +39 -13
  81. package/dist/host.js +268 -142
  82. package/dist/index.d.ts +25 -15
  83. package/dist/index.js +39 -16
  84. package/dist/install-release-cli.d.ts +2 -0
  85. package/dist/install-release-cli.js +46 -0
  86. package/dist/installation-client.d.ts +43 -0
  87. package/dist/installation-client.js +178 -0
  88. package/dist/installation-execution.d.ts +11 -0
  89. package/dist/installation-execution.js +184 -0
  90. package/dist/installation-operations.d.ts +1437 -0
  91. package/dist/installation-operations.js +664 -0
  92. package/dist/installation-store-generation.d.ts +3 -0
  93. package/dist/installation-store-generation.js +16 -0
  94. package/dist/instance-id.d.ts +10 -0
  95. package/dist/instance-id.js +10 -0
  96. package/dist/instance-refusal.d.ts +11 -0
  97. package/dist/instance-refusal.js +21 -0
  98. package/dist/internal/delivery.js +4 -2
  99. package/dist/internal/host-gate.js +7 -1
  100. package/dist/internal/registry-installations.js +4 -4
  101. package/dist/keys.d.ts +5 -1
  102. package/dist/keys.js +10 -2
  103. package/dist/machine-link/authority.d.ts +89 -0
  104. package/dist/machine-link/authority.js +37 -0
  105. package/dist/machine-link/client.d.ts +66 -0
  106. package/dist/machine-link/client.js +94 -0
  107. package/dist/machine-link/delivery.d.ts +18 -0
  108. package/dist/machine-link/delivery.js +55 -0
  109. package/dist/machine-link/enrollment.d.ts +111 -0
  110. package/dist/machine-link/enrollment.js +286 -0
  111. package/dist/machine-link/fences.d.ts +37 -0
  112. package/dist/machine-link/fences.js +166 -0
  113. package/dist/machine-link/home.d.ts +20 -0
  114. package/dist/machine-link/home.js +24 -0
  115. package/dist/machine-link/host.d.ts +43 -0
  116. package/dist/machine-link/host.js +189 -0
  117. package/dist/machine-link/index.d.ts +15 -0
  118. package/dist/machine-link/index.js +15 -0
  119. package/dist/machine-link/link.d.ts +48 -0
  120. package/dist/machine-link/link.js +223 -0
  121. package/dist/machine-link/presence.d.ts +10 -0
  122. package/dist/machine-link/presence.js +25 -0
  123. package/dist/machine-link/workers.d.ts +6 -0
  124. package/dist/machine-link/workers.js +6 -0
  125. package/dist/mcp-owner.d.ts +2 -2
  126. package/dist/mcp-owner.js +41 -33
  127. package/dist/mcp.d.ts +15 -3
  128. package/dist/mcp.js +146 -25
  129. package/dist/pack.d.ts +14 -3
  130. package/dist/pack.js +44 -19
  131. package/dist/page/app-access.d.ts +7 -0
  132. package/dist/page/app-access.js +75 -0
  133. package/dist/page/boot-host.js +57 -0
  134. package/dist/page/capabilities.d.ts +3 -3
  135. package/dist/page/capabilities.js +16 -12
  136. package/dist/page/computer-handles.d.ts +3 -3
  137. package/dist/page/computer-target.d.ts +3 -0
  138. package/dist/page/computer-target.js +9 -0
  139. package/dist/page/mount.d.ts +23 -3
  140. package/dist/page/mount.js +152 -16
  141. package/dist/page-assets.d.ts +22 -0
  142. package/dist/page-assets.js +28 -0
  143. package/dist/page-script.d.ts +3 -0
  144. package/dist/page-script.js +5 -4
  145. package/dist/release.d.ts +13 -6
  146. package/dist/release.js +10 -7
  147. package/dist/root-data.d.ts +16 -6
  148. package/dist/root-data.js +2 -2
  149. package/dist/source/index.d.ts +1 -1
  150. package/dist/source/team-source.d.ts +20 -8
  151. package/dist/source/team-source.js +39 -10
  152. package/dist/source/template-seed.d.ts +2 -2
  153. package/dist/standalone/agent-mail.d.ts +24 -0
  154. package/dist/standalone/agent-mail.js +62 -0
  155. package/dist/standalone/agent-store.d.ts +9 -0
  156. package/dist/standalone/agent-store.js +30 -0
  157. package/dist/standalone/agents.d.ts +68 -9
  158. package/dist/standalone/agents.js +428 -56
  159. package/dist/standalone/app-builds-binding.d.ts +20 -2
  160. package/dist/standalone/app-builds-binding.js +147 -23
  161. package/dist/standalone/app-set.d.ts +5 -2
  162. package/dist/standalone/app-set.js +174 -143
  163. package/dist/standalone/browser.d.ts +11 -0
  164. package/dist/standalone/browser.js +72 -0
  165. package/dist/standalone/builds.d.ts +13 -4
  166. package/dist/standalone/builds.js +28 -44
  167. package/dist/standalone/computer-egress.d.ts +2 -0
  168. package/dist/standalone/computer-egress.js +13 -4
  169. package/dist/standalone/computer-fleet.d.ts +43 -0
  170. package/dist/standalone/computer-fleet.js +200 -0
  171. package/dist/standalone/computer.d.ts +44 -9
  172. package/dist/standalone/computer.js +129 -22
  173. package/dist/standalone/install-apps-cli.js +43 -6
  174. package/dist/standalone/instance.d.ts +2 -2
  175. package/dist/standalone/instance.js +8 -11
  176. package/dist/standalone/machine-link.d.ts +354 -0
  177. package/dist/standalone/machine-link.js +368 -0
  178. package/dist/standalone/media.d.ts +2 -2
  179. package/dist/standalone/media.js +19 -16
  180. package/dist/standalone/preview.d.ts +2 -2
  181. package/dist/standalone/preview.js +37 -10
  182. package/dist/standalone/registry.d.ts +149 -9
  183. package/dist/standalone/registry.js +299 -8
  184. package/dist/standalone/worker.d.ts +19 -5
  185. package/dist/standalone/worker.js +90 -54
  186. package/dist/standalone/workflow-binding.d.ts +18 -22
  187. package/dist/standalone/workflow-binding.js +18 -18
  188. package/dist/standalone/workflow-control.d.ts +4 -0
  189. package/dist/standalone/workflow-control.js +40 -0
  190. package/dist/standalone/workflow-events.d.ts +2 -2
  191. package/dist/standalone/workflow-events.js +9 -10
  192. package/dist/standalone/workflow-git-capture.js +4 -3
  193. package/dist/standalone/workflow-host.d.ts +162 -161
  194. package/dist/standalone/workflow-host.js +101 -21
  195. package/dist/standalone/workflow-node-capture.js +11 -4
  196. package/dist/standalone/workflow-schedules.d.ts +5 -2
  197. package/dist/standalone/workflow-schedules.js +9 -10
  198. package/dist/standalone/workflow-trigger-host.d.ts +65 -5
  199. package/dist/standalone/workflow-trigger-host.js +2 -2
  200. package/dist/standalone/workflow-webhooks.d.ts +22 -0
  201. package/dist/standalone/workflow-webhooks.js +39 -0
  202. package/dist/standalone/workflows.d.ts +140 -43
  203. package/dist/standalone/workflows.js +198 -39
  204. package/dist/storage-frame.d.ts +6 -6
  205. package/dist/testing/capabilities.js +14 -3
  206. package/dist/testing/installation.js +26 -2
  207. package/dist/testing/worker.js +1 -1
  208. package/dist/testing/workers-runtime.d.ts +8 -2
  209. package/dist/testing/workers-runtime.js +20 -2
  210. package/dist/webhook-abuse.d.ts +25 -0
  211. package/dist/webhook-abuse.js +54 -0
  212. package/dist/webhook-custody.d.ts +144 -0
  213. package/dist/webhook-custody.js +222 -0
  214. package/dist/webhook-ingress.d.ts +36 -0
  215. package/dist/webhook-ingress.js +146 -0
  216. package/dist/webhooks.d.ts +111 -0
  217. package/dist/webhooks.js +189 -0
  218. package/dist/workers-git.d.ts +12 -17
  219. package/dist/workers-git.js +60 -52
  220. package/dist/workers.d.ts +224 -13
  221. package/dist/workers.js +351 -51
  222. package/dist/workflow-agent-authority.d.ts +8 -0
  223. package/dist/workflow-agent-authority.js +42 -0
  224. package/dist/workflows.d.ts +66 -0
  225. package/dist/workflows.js +97 -0
  226. package/docs/api/README.md +63 -37
  227. package/docs/api/agents.md +17 -0
  228. package/docs/api/app-storage.md +1 -1
  229. package/docs/api/apps.md +1394 -543
  230. package/docs/api/browser/boot.md +132 -0
  231. package/docs/api/browser.md +1440 -0
  232. package/docs/api/build-contract.md +38 -25
  233. package/docs/api/build.md +263 -84
  234. package/docs/api/cloudflare.md +93 -7
  235. package/docs/api/git.md +3 -13
  236. package/docs/api/host.md +944 -118
  237. package/docs/api/index.md +320 -18
  238. package/docs/api/keys.md +24 -1
  239. package/docs/api/mcp.md +59 -12
  240. package/docs/api/modules.md +33 -40
  241. package/docs/api/pack.md +57 -8
  242. package/docs/api/page/mount.md +92 -4
  243. package/docs/api/workers-git.md +103 -24
  244. package/docs/api/workers.md +1581 -244
  245. package/docs/site/agent-quickstart.mdx +28 -14
  246. package/examples/standalone/worker.ts +3 -1
  247. package/examples/standalone/wrangler.jsonc +2 -0
  248. package/package.json +73 -71
  249. package/dist/installation-consent.d.ts +0 -81
  250. package/dist/installation-consent.js +0 -84
  251. package/dist/shell/app-directory.d.ts +0 -542
  252. package/dist/shell/app-directory.js +0 -196
  253. package/dist/shell/browser-actions.d.ts +0 -7
  254. package/dist/shell/browser-actions.js +0 -56
  255. package/dist/shell/command-host.d.ts +0 -112
  256. package/dist/shell/command-host.js +0 -502
  257. package/dist/shell/commands.d.ts +0 -25
  258. package/dist/shell/commands.js +0 -78
  259. package/dist/shell/index.d.ts +0 -8
  260. package/dist/shell/index.js +0 -8
  261. package/dist/shell/keys.d.ts +0 -41
  262. package/dist/shell/keys.js +0 -134
  263. package/dist/shell/shell-module.d.ts +0 -55
  264. package/dist/shell/shell-module.js +0 -86
  265. package/dist/shell/window-manager.d.ts +0 -128
  266. package/dist/shell/window-manager.js +0 -730
  267. package/dist/shell/workbench-contract.d.ts +0 -68
  268. package/dist/shell/workbench-directory.d.ts +0 -332
  269. package/dist/shell/workbench-directory.js +0 -35
  270. package/dist/shell/workbench-host-types.d.ts +0 -96
  271. package/dist/shell/workbench-host-types.js +0 -1
  272. package/dist/shell/workbench-host.d.ts +0 -34
  273. package/dist/shell/workbench-host.js +0 -352
  274. package/dist/shell/workbench-model.d.ts +0 -194
  275. package/dist/shell/workbench-model.js +0 -53
  276. package/dist/shell/workbench.d.ts +0 -4
  277. package/dist/shell/workbench.js +0 -2
  278. package/docs/api/shell/app-directory.md +0 -599
  279. package/docs/api/shell/command-host.md +0 -224
  280. package/docs/api/shell/commands.md +0 -197
  281. package/docs/api/shell/keys.md +0 -183
  282. package/docs/api/shell/shell-module.md +0 -204
  283. package/docs/api/shell/window-manager.md +0 -267
  284. package/docs/api/shell/workbench-directory.md +0 -119
  285. package/docs/api/shell/workbench.md +0 -205
  286. package/docs/api/shell.md +0 -1915
  287. /package/dist/{shell/workbench-contract.js → page/boot-host.d.ts} +0 -0
@@ -0,0 +1,10 @@
1
+ import { type CairnId } from "@fungi.computer/cairn/id";
2
+ import * as Schema from "effect/Schema";
3
+ /** The Cairn ID family that mints and recognizes App Instance IDs (`instance_…`). */
4
+ export declare const instanceIds: Readonly<{
5
+ readonly instance: import("@fungi.computer/cairn").IdCodec<"app-instance", "instance", 1>;
6
+ }>;
7
+ /** A stable App Instance ID minted by {@link instanceIds}. */
8
+ export type InstanceId = CairnId<"app-instance", "instance">;
9
+ /** Decodes a string into an {@link InstanceId}, rejecting any other ID family. */
10
+ export declare const instanceIdSchema: Schema.Codec<InstanceId, string>;
@@ -0,0 +1,10 @@
1
+ import { defineIdFamily } from "@fungi.computer/cairn/id";
2
+ import * as Schema from "effect/Schema";
3
+ /** The Cairn ID family that mints and recognizes App Instance IDs (`instance_…`). */
4
+ export const instanceIds = defineIdFamily({
5
+ family: "app-instance",
6
+ version: 1,
7
+ kinds: { instance: { prefix: "instance" } },
8
+ });
9
+ /** Decodes a string into an {@link InstanceId}, rejecting any other ID family. */
10
+ export const instanceIdSchema = Schema.String.pipe(Schema.refine((value) => instanceIds.instance.is(value)));
@@ -0,0 +1,11 @@
1
+ import type { AppInstanceErrorCode } from "./index.js";
2
+ /** The retained HTTP meaning of a known Instance refusal, never generic failed. */
3
+ export declare function instanceRefusalStatus(code: AppInstanceErrorCode): number;
4
+ /** Only closed host-authored boot reasons produce a caller-facing message. */
5
+ export declare function instanceRefusalBody(code: string): {
6
+ error: "backend_load_failed" | "backend_construction_failed" | "ui_mount_failed" | "boot_runner_unavailable" | "boot_browser_binding_missing" | "boot_clone_unavailable" | "boot_timeout" | "boot_cleanup_failed";
7
+ message: string;
8
+ } | {
9
+ error: string;
10
+ message?: undefined;
11
+ };
@@ -0,0 +1,21 @@
1
+ import { BootGateError, bootFailureSchema } from "./boot-gate.js";
2
+ const conflicts = new Set([
3
+ "conflicting_owner_state",
4
+ "stale_authority",
5
+ "instance_release_mismatch",
6
+ "release_no_longer_matches_instance",
7
+ "state_schema_migration_unsupported",
8
+ ]);
9
+ /** The retained HTTP meaning of a known Instance refusal, never generic failed. */
10
+ export function instanceRefusalStatus(code) {
11
+ if (bootFailureSchema.safeParse(code).success)
12
+ return 422;
13
+ return conflicts.has(code) ? 409 : 403;
14
+ }
15
+ /** Only closed host-authored boot reasons produce a caller-facing message. */
16
+ export function instanceRefusalBody(code) {
17
+ const boot = bootFailureSchema.safeParse(code);
18
+ return boot.success
19
+ ? { error: boot.data, message: new BootGateError(boot.data).message }
20
+ : { error: code };
21
+ }
@@ -22,7 +22,8 @@ async function admitGitPush(host, readRelease, installationId, raw) {
22
22
  const current = await host.installations.get(installationId);
23
23
  if (current?.status !== "enabled" ||
24
24
  current.instanceId !== installation.instanceId ||
25
- current.authorityVersion !== installation.authorityVersion ||
25
+ current.authority.authorityVersion !==
26
+ installation.authority.authorityVersion ||
26
27
  current.release.releaseId !== installation.release.releaseId)
27
28
  throw new Error("Git push installation changed");
28
29
  return { installation: current, release, fact };
@@ -45,8 +46,9 @@ function deliverAdmittedGitPush(host, installation, release, fact) {
45
46
  * this path never calls the producer's optional enqueue callback. */
46
47
  export async function deliverGitPush(host, installationId, fact) {
47
48
  const admitted = await admitGitPush(host, (release) => verifyRelease(host.artifacts, release, defaultAssetPolicy, false), installationId, fact);
48
- if (admitted !== null)
49
+ if (admitted !== null) {
49
50
  await deliverAdmittedGitPush(host, admitted.installation, admitted.release, admitted.fact);
51
+ }
50
52
  }
51
53
  /** Re-admit a stored member through the same directory and method gate as a click. */
52
54
  export async function deliverBackground(host, raw) {
@@ -45,7 +45,13 @@ export async function hostGateFacts(host, installation, release, principal) {
45
45
  const declaration = release.declaration ?? emptyAppDeclaration;
46
46
  const toolPolicy = await hostToolPolicy(host, actor, installation.installationId);
47
47
  return {
48
- installation,
48
+ installation: {
49
+ status: installation.status,
50
+ visibility: installation.visibility,
51
+ instanceId: installation.instanceId,
52
+ authority: installation.authority,
53
+ artifactDigest: installation.release.artifact.artifactDigest,
54
+ },
49
55
  declaration,
50
56
  declaredOperations: await deriveDeclaredOperations(installation.release, declaration, host.capabilities ?? []),
51
57
  api: release.api,
@@ -1,12 +1,12 @@
1
+ import { installationOperationsPort } from "../installation-operations.js";
1
2
  /** Read and commit installations in the scope's registry. */
2
3
  export function registryInstallations(env, scope) {
3
4
  const registry = () => env.REGISTRY.getByName(scope.durableName("registry"));
4
5
  return {
6
+ operations: installationOperationsPort((request) => registry().installationOperation(request), { tenantId: scope.tenantId, origin: scope.origin }),
7
+ generation: async () => registry().generation(),
5
8
  list: async () => registry().list(),
6
9
  get: async (id) => registry().get(id),
7
- put: async (installation) => {
8
- await registry().put(installation, scope.tenantId);
9
- },
10
- compareAndPut: async (installation, expectedAuthorityVersion) => await registry().compareAndPut(installation, expectedAuthorityVersion, scope.tenantId),
10
+ compareAndPut: async (installation, expectedAuthorityVersion, expectedGeneration) => await registry().compareAndPut(installation, expectedAuthorityVersion, expectedGeneration, scope.tenantId),
11
11
  };
12
12
  }
package/dist/keys.d.ts CHANGED
@@ -2,12 +2,14 @@ import type { Grant, GrantVocabulary, Scope } from "@fungi.computer/ear-tag/gran
2
2
  import type { KeyPrincipal, KeyRecord, KeyStore } from "@fungi.computer/ear-tag/keys";
3
3
  import type { AppToolPolicy } from "./apps/tools.js";
4
4
  /** Every capability an Oyster key may carry. */
5
- export type OysterKeyCapability = "apps.read" | "apps.use" | "computer.use";
5
+ export type OysterKeyCapability = "apps.read" | "apps.use" | "computer.use" | "machine.enroll";
6
6
  /**
7
7
  * `apps.use` also reaches `apps.read` (one level, per `defineGrants`'s own
8
8
  * `implies`): a key that may change an App's data may also read it.
9
9
  * `computer.use` has no narrower capability under it; the Computer is on or
10
10
  * off, host-wide.
11
+ * `machine.enroll` admits enrollment only; it implies no App or Computer access.
12
+ * The enrollment owner, not this vocabulary, owns one-use consumption.
11
13
  */
12
14
  export declare const oysterKeyVocabulary: GrantVocabulary<OysterKeyCapability>;
13
15
  /** A grant reaching every current and future installation for `capability`. */
@@ -46,3 +48,5 @@ export type LiveOysterKey = Readonly<{
46
48
  export declare function keyAppAccess(key: LiveOysterKey, installationId: string): AppToolPolicy | null;
47
49
  /** Whether a live key's own grants turn its Computer access on. Off by default. */
48
50
  export declare function keyAllowsComputerUse(key: LiveOysterKey): boolean;
51
+ /** Whether a live key's grants permit a one-use device enrollment. */
52
+ export declare function keyAllowsMachineEnroll(key: LiveOysterKey): boolean;
package/dist/keys.js CHANGED
@@ -1,6 +1,6 @@
1
1
  /**
2
2
  * Oyster's own Ear Tag vocabulary: named keys an owner issues for its
3
- * installed Apps and, optionally, its Computer. A key's id is its name, so
3
+ * installed Apps, its Computer or machine enrollment. A key's id is its name, so
4
4
  * forks and other name-derived identities stay stable across the cutover.
5
5
  */
6
6
  import { defineGrants } from "@fungi.computer/ear-tag/grants";
@@ -9,9 +9,11 @@ import { defineGrants } from "@fungi.computer/ear-tag/grants";
9
9
  * `implies`): a key that may change an App's data may also read it.
10
10
  * `computer.use` has no narrower capability under it; the Computer is on or
11
11
  * off, host-wide.
12
+ * `machine.enroll` admits enrollment only; it implies no App or Computer access.
13
+ * The enrollment owner, not this vocabulary, owns one-use consumption.
12
14
  */
13
15
  export const oysterKeyVocabulary = defineGrants({
14
- capabilities: ["apps.read", "apps.use", "computer.use"],
16
+ capabilities: ["apps.read", "apps.use", "computer.use", "machine.enroll"],
15
17
  implies: { "apps.use": ["apps.read"] },
16
18
  });
17
19
  /** A grant reaching every current and future installation for `capability`. */
@@ -50,3 +52,9 @@ export function keyAppAccess(key, installationId) {
50
52
  export function keyAllowsComputerUse(key) {
51
53
  return oysterKeyVocabulary.grantsAllow(key.grants, "computer.use");
52
54
  }
55
+ /** Whether a live key's grants permit a one-use device enrollment. */
56
+ export function keyAllowsMachineEnroll(key) {
57
+ return (key.grants.length === 1 &&
58
+ key.grants[0]?.capability === "machine.enroll" &&
59
+ oysterKeyVocabulary.grantsAllow(key.grants, "machine.enroll"));
60
+ }
@@ -0,0 +1,89 @@
1
+ import { z } from "zod";
2
+ import type { PostDeliveryResult, PostEnvelope } from "@fungi.computer/carrier/post";
3
+ import type { MachineAgentHome } from "./home.js";
4
+ export declare const machineLinkIdentitySchema: z.ZodObject<{
5
+ teamId: z.ZodString;
6
+ deviceId: z.ZodString;
7
+ }, z.core.$strict>;
8
+ export type MachineLinkIdentity = z.infer<typeof machineLinkIdentitySchema>;
9
+ /** One canonical device owner, not a Team's socket collection. */
10
+ export declare function machineLinkName(identity: MachineLinkIdentity): string;
11
+ /** One device's live authority, as the host answers it for one act. */
12
+ export type MachineLinkDeviceFacts = Readonly<{
13
+ live: false;
14
+ }> | Readonly<MachineLinkIdentity & {
15
+ live: true;
16
+ memberId: string;
17
+ publicKey: string;
18
+ fenceEpoch: number;
19
+ }>;
20
+ /** Retained authority, including a revoked device's last committed fence. */
21
+ export type MachineLinkFenceFact = Readonly<MachineLinkIdentity & {
22
+ live: boolean;
23
+ fenceEpoch: number;
24
+ }>;
25
+ export declare const machineLinkFenceResultSchema: z.ZodObject<{
26
+ kind: z.ZodEnum<{
27
+ unconfirmed: "unconfirmed";
28
+ confirmed: "confirmed";
29
+ }>;
30
+ fenceEpoch: z.ZodInt;
31
+ }, z.core.$strict>;
32
+ export type MachineLinkFenceResult = z.infer<typeof machineLinkFenceResultSchema>;
33
+ /** Device identity comes from the forwarding link's pin, never the mail body. */
34
+ export declare const machineLinkCallerSchema: z.ZodDiscriminatedUnion<[z.ZodObject<{
35
+ kind: z.ZodLiteral<"hosted">;
36
+ agentId: z.ZodString;
37
+ }, z.core.$strict>, z.ZodObject<{
38
+ kind: z.ZodLiteral<"device">;
39
+ deviceId: z.ZodString;
40
+ agentId: z.ZodString;
41
+ }, z.core.$strict>], "kind">;
42
+ export type MachineLinkCaller = z.infer<typeof machineLinkCallerSchema>;
43
+ export declare const machineLinkFenceFactSchema: z.ZodObject<{
44
+ teamId: z.ZodString;
45
+ deviceId: z.ZodString;
46
+ live: z.ZodBoolean;
47
+ fenceEpoch: z.ZodInt;
48
+ }, z.core.$strict>;
49
+ /** The host's current facts for one addressed operation or mail. */
50
+ export type MachineLinkTargetFacts = Readonly<{
51
+ teamId: string;
52
+ callerAgentId: string;
53
+ targetAgentId: string;
54
+ /** The Agent the target acts for: the settlement requester, or the caller. */
55
+ actorAgentId: string;
56
+ /** The caller's own home; the link checks it against the caller it saw. */
57
+ callerHome: MachineAgentHome;
58
+ targetHome: MachineAgentHome;
59
+ }>;
60
+ /**
61
+ * Everything a machine link asks its host. Every method is a fresh live read
62
+ * for one dial, frame, delivery or operation; the link never caches an answer.
63
+ * The Hub answers through its Team control boundary, a standalone Oyster
64
+ * through its owner's key and grants.
65
+ */
66
+ export type MachineLinkAuthority = Readonly<{
67
+ /** Device present and unrevoked, owner/member live, current key and epoch. */
68
+ admitDevice(id: MachineLinkIdentity): Promise<MachineLinkDeviceFacts>;
69
+ /** Retained fence facts, including a revoked device; null for an unknown one. */
70
+ currentFence(id: MachineLinkIdentity): Promise<MachineLinkFenceFact | null>;
71
+ /** Public Agent entries this device may see. */
72
+ directory(id: MachineLinkIdentity): Promise<readonly {
73
+ agentId: string;
74
+ name: string;
75
+ }[]>;
76
+ /** Sender, actor and the target's actual home, rechecked for this act. */
77
+ resolveTarget(id: MachineLinkIdentity, caller: MachineLinkCaller, agentId: string, requesterAgentId: string | null): Promise<MachineLinkTargetFacts>;
78
+ /** Carry mail received from this device to its target's actual home. */
79
+ deliverHome(id: MachineLinkIdentity, caller: MachineLinkCaller, target: MachineLinkTargetFacts, envelope: PostEnvelope): Promise<PostDeliveryResult>;
80
+ /** Whether this link's replay ledger may be erased now. */
81
+ canErase(id: MachineLinkIdentity): Promise<boolean>;
82
+ /** Push one committed fence fact to the device's link (C2 promptness). */
83
+ deliverControlFact(fact: MachineLinkFenceFact): Promise<MachineLinkFenceResult>;
84
+ }>;
85
+ /** A link refusal, decoded by the host into its own error vocabulary. */
86
+ export declare class MachineLinkRefused extends Error {
87
+ readonly reason: "actor_not_allowed" | "unavailable";
88
+ constructor(reason: "actor_not_allowed" | "unavailable");
89
+ }
@@ -0,0 +1,37 @@
1
+ import { z } from "zod";
2
+ const id = z.string().min(1).max(256);
3
+ export const machineLinkIdentitySchema = z.strictObject({
4
+ teamId: id,
5
+ deviceId: id,
6
+ });
7
+ /** One canonical device owner, not a Team's socket collection. */
8
+ export function machineLinkName(identity) {
9
+ return JSON.stringify(machineLinkIdentitySchema.parse({
10
+ teamId: identity.teamId,
11
+ deviceId: identity.deviceId,
12
+ }));
13
+ }
14
+ export const machineLinkFenceResultSchema = z.strictObject({
15
+ kind: z.enum(["confirmed", "unconfirmed"]),
16
+ fenceEpoch: z.int().nonnegative(),
17
+ });
18
+ /** Device identity comes from the forwarding link's pin, never the mail body. */
19
+ export const machineLinkCallerSchema = z.discriminatedUnion("kind", [
20
+ z.strictObject({ kind: z.literal("hosted"), agentId: id }),
21
+ z.strictObject({ kind: z.literal("device"), deviceId: id, agentId: id }),
22
+ ]);
23
+ export const machineLinkFenceFactSchema = z.strictObject({
24
+ teamId: id,
25
+ deviceId: id,
26
+ live: z.boolean(),
27
+ fenceEpoch: z.int().nonnegative(),
28
+ });
29
+ /** A link refusal, decoded by the host into its own error vocabulary. */
30
+ export class MachineLinkRefused extends Error {
31
+ reason;
32
+ constructor(reason) {
33
+ super(`machine-link:${reason}`);
34
+ this.reason = reason;
35
+ this.name = "MachineLinkRefused";
36
+ }
37
+ }
@@ -0,0 +1,66 @@
1
+ import type * as Schema from "effect/Schema";
2
+ import type { PostEnvelope, PostDeliveryResult } from "@fungi.computer/carrier/post";
3
+ import type { MachineAgentOperation } from "@fungi.computer/seam/machine-link-protocol";
4
+ import type { MachineAgentHome } from "./home.js";
5
+ import type { MachineLink } from "./link.js";
6
+ import { type MachineLinkCaller, type MachineLinkIdentity } from "./authority.js";
7
+ /** The caller a hosted settlement read acts for. */
8
+ export type MachineAgentSettlementPrincipal = Readonly<{
9
+ teamId: string;
10
+ actor: Readonly<{
11
+ kind: "agent";
12
+ id: string;
13
+ }>;
14
+ }>;
15
+ /**
16
+ * The host's Agent wire grammar. This module routes Agent work by home and
17
+ * never decodes an Agent body itself. Each codec must be strict: it is the
18
+ * only decoder of caller bodies and of settlements a device returns.
19
+ */
20
+ export type MachineAgentCodecs<Settlement> = Readonly<{
21
+ /** The Session-creation intent, or null when the body is not one. */
22
+ sessionCreation(body: Schema.Json): Readonly<{
23
+ requestId: string;
24
+ }> | null;
25
+ /** One serialized settlement from a hosted or machine-homed Agent. */
26
+ settlement(encoded: string): Settlement;
27
+ /** Throws unless an acknowledgment carries a well-formed Accepted receipt. */
28
+ accepted(receipt: Schema.Json): void;
29
+ }>;
30
+ /** The device link entry points a hosted caller needs. */
31
+ export type MachineAgentLinkPort = Readonly<{
32
+ deliver(identity: MachineLinkIdentity, caller: MachineLinkCaller, agentId: string, envelope: PostEnvelope): Promise<PostDeliveryResult>;
33
+ operation(identity: MachineLinkIdentity, caller: MachineLinkCaller, agentId: string, operation: MachineAgentOperation): ReturnType<MachineLink["operation"]>;
34
+ }>;
35
+ /**
36
+ * Actual-home routing for a hosted Agent's mail, Session creation and
37
+ * settlement reads. A machine-homed target goes to its device link and never
38
+ * falls back to a hosted Agent; the host supplies the fresh home read.
39
+ */
40
+ export declare function createMachineAgentClient<Settlement>(options: {
41
+ identity: () => Readonly<{
42
+ teamId: string;
43
+ agentId: string;
44
+ }> | null;
45
+ links: {
46
+ getByName(name: string): MachineAgentLinkPort;
47
+ };
48
+ /** The target's current home, read fresh for this act. */
49
+ home: (agentId: string) => Promise<MachineAgentHome>;
50
+ codecs: MachineAgentCodecs<Settlement>;
51
+ hosted: {
52
+ getByName(name: string): {
53
+ deliver(envelope: PostEnvelope): Promise<PostDeliveryResult>;
54
+ fetch(request: Request): Promise<Response>;
55
+ settlement(principal: MachineAgentSettlementPrincipal, sessionId: string, commandId: string): Promise<string | undefined>;
56
+ };
57
+ };
58
+ }): {
59
+ namespace: {
60
+ getByName: (agentId: string) => {
61
+ deliver: (envelope: PostEnvelope) => Promise<PostDeliveryResult>;
62
+ };
63
+ };
64
+ dispatch: (agentId: string, request: Request) => Promise<Response>;
65
+ settlement: (agentId: string, sessionId: string, commandId: string, requesterAgentId: string) => Promise<Settlement | undefined>;
66
+ };
@@ -0,0 +1,94 @@
1
+ import { Hono } from "hono";
2
+ import { bodyLimit } from "hono/body-limit";
3
+ import { validator } from "hono/validator";
4
+ import { z } from "zod";
5
+ import { createHomeAwareAgentMailNamespace } from "./delivery.js";
6
+ import { machineLinkName, } from "./authority.js";
7
+ const identity = z.string().min(1).max(256);
8
+ /**
9
+ * Actual-home routing for a hosted Agent's mail, Session creation and
10
+ * settlement reads. A machine-homed target goes to its device link and never
11
+ * falls back to a hosted Agent; the host supplies the fresh home read.
12
+ */
13
+ export function createMachineAgentClient(options) {
14
+ function actor() {
15
+ const current = options.identity();
16
+ if (current === null)
17
+ throw new Error("Agent Team identity is unavailable");
18
+ return current;
19
+ }
20
+ const home = (agentId) => options.home(agentId);
21
+ const machine = (deviceId) => {
22
+ const current = actor();
23
+ const identity = { teamId: current.teamId, deviceId };
24
+ const caller = {
25
+ kind: "hosted",
26
+ agentId: current.agentId,
27
+ };
28
+ return {
29
+ identity,
30
+ caller,
31
+ link: options.links.getByName(machineLinkName(identity)),
32
+ };
33
+ };
34
+ return {
35
+ namespace: {
36
+ getByName: (agentId) => ({
37
+ deliver: async (envelope) => {
38
+ return await createHomeAwareAgentMailNamespace({
39
+ sender: actor(),
40
+ home,
41
+ hosted: options.hosted,
42
+ accepted: options.codecs.accepted,
43
+ machine: (deviceId, cargo) => {
44
+ const target = machine(deviceId);
45
+ return target.link.deliver(target.identity, target.caller, agentId, cargo);
46
+ },
47
+ })
48
+ .getByName(agentId)
49
+ .deliver(envelope);
50
+ },
51
+ }),
52
+ },
53
+ dispatch: async (agentId, request) => {
54
+ const targetHome = await home(agentId);
55
+ if (targetHome.kind === "hosted")
56
+ return await options.hosted.getByName(agentId).fetch(request);
57
+ const routes = new Hono().post("/sessions", bodyLimit({ maxSize: 512 * 1024 }),
58
+ // Hono hands over the parsed JSON body; the host codec decodes it.
59
+ validator("json", (value, context) => {
60
+ const parsed = options.codecs.sessionCreation(value);
61
+ if (parsed === null)
62
+ return context.json({ error: "invalid" }, 400);
63
+ return parsed;
64
+ }), async (context) => {
65
+ const target = machine(targetHome.deviceId);
66
+ const result = z.strictObject({ sessionId: identity }).parse(await target.link.operation(target.identity, target.caller, agentId, {
67
+ type: "createSession",
68
+ requestId: context.req.valid("json").requestId,
69
+ }));
70
+ return context.json(result, 201);
71
+ });
72
+ return await routes.fetch(request);
73
+ },
74
+ settlement: async (agentId, sessionId, commandId, requesterAgentId) => {
75
+ const targetHome = await home(agentId);
76
+ const current = actor();
77
+ const target = targetHome.kind === "machine" ? machine(targetHome.deviceId) : null;
78
+ const encoded = target === null
79
+ ? await options.hosted.getByName(agentId).settlement({
80
+ teamId: current.teamId,
81
+ actor: { kind: "agent", id: requesterAgentId },
82
+ }, sessionId, commandId)
83
+ : (z.strictObject({ settlement: z.string().nullable() }).parse(await target.link.operation(target.identity, target.caller, agentId, {
84
+ type: "settlement",
85
+ requesterAgentId,
86
+ sessionId,
87
+ commandId,
88
+ })).settlement ?? undefined);
89
+ return encoded === undefined
90
+ ? undefined
91
+ : options.codecs.settlement(encoded);
92
+ },
93
+ };
94
+ }
@@ -0,0 +1,18 @@
1
+ import * as Schema from "effect/Schema";
2
+ import { type PostEnvelope, type PostDeliveryResult, type PostRoute } from "@fungi.computer/carrier/post";
3
+ import { type MachineAgentHome } from "./home.js";
4
+ /**
5
+ * Home selection only, as the namespace under the host's disclosure-owning
6
+ * Agent mail route. Carrier retains retries; the receiving Agent admits work.
7
+ */
8
+ export declare function createHomeAwareAgentMailNamespace(options: {
9
+ sender: Readonly<{
10
+ teamId: string;
11
+ agentId: string;
12
+ }>;
13
+ home: (agentId: string) => Promise<MachineAgentHome | null>;
14
+ hosted: PostRoute<never, never>["namespace"];
15
+ machine: (deviceId: string, envelope: PostEnvelope) => Promise<PostDeliveryResult>;
16
+ /** Host's Agent grammar: throws unless the receipt is a well-formed Accepted. */
17
+ accepted: (receipt: Schema.Json) => void;
18
+ }): PostRoute<never, never>["namespace"];
@@ -0,0 +1,55 @@
1
+ import * as Schema from "effect/Schema";
2
+ import { BirdDogMailSchema } from "@fungi.computer/bird-dog/effect";
3
+ import { PostEnvelopeSchema, PostDeliveryResultSchema, } from "@fungi.computer/carrier/post";
4
+ import { agentMailAddressSchema } from "@fungi.computer/seam/machine-link-protocol";
5
+ import { decodeMachineAgentHome } from "./home.js";
6
+ /**
7
+ * Home selection only, as the namespace under the host's disclosure-owning
8
+ * Agent mail route. Carrier retains retries; the receiving Agent admits work.
9
+ */
10
+ export function createHomeAwareAgentMailNamespace(options) {
11
+ return {
12
+ getByName: (agentId) => ({
13
+ deliver: async (raw) => {
14
+ if (new TextEncoder().encode(JSON.stringify(raw)).byteLength >
15
+ 512 * 1024)
16
+ return "refused";
17
+ const envelope = Schema.decodeUnknownSync(PostEnvelopeSchema, {
18
+ onExcessProperty: "error",
19
+ })(raw);
20
+ const address = Schema.decodeUnknownSync(agentMailAddressSchema, {
21
+ onExcessProperty: "error",
22
+ })(envelope.address);
23
+ const mail = Schema.decodeUnknownSync(BirdDogMailSchema, {
24
+ onExcessProperty: "error",
25
+ })(envelope.event);
26
+ if (envelope.kind !== "agentMail" ||
27
+ envelope.key !== mail.key ||
28
+ address.scopeId !== options.sender.teamId ||
29
+ address.agentId !== agentId ||
30
+ mail.to.agentId !== agentId ||
31
+ mail.from.agentId !== options.sender.agentId ||
32
+ new TextEncoder().encode(JSON.stringify(mail)).byteLength > 256 * 1024)
33
+ return "refused";
34
+ const projected = await options.home(agentId);
35
+ if (projected === null)
36
+ return "refused";
37
+ const home = decodeMachineAgentHome(projected);
38
+ // Unavailable machine transport must throw for Carrier retry, never
39
+ // fall back to a hosted Agent or manufacture a custody/command receipt.
40
+ const result = Schema.decodeUnknownSync(PostDeliveryResultSchema)(await (home.kind === "hosted"
41
+ ? options.hosted.getByName(agentId).deliver(envelope)
42
+ : options.machine(home.deviceId, envelope)));
43
+ if (result === "refused")
44
+ return result;
45
+ if (result === "accepted" ||
46
+ result === "duplicate" ||
47
+ !("receipt" in result))
48
+ throw new Error("Agent mail acknowledgment lacks an Accepted receipt");
49
+ if (result.status !== "refused")
50
+ options.accepted(result.receipt);
51
+ return result;
52
+ },
53
+ }),
54
+ };
55
+ }
@@ -0,0 +1,111 @@
1
+ import type { MachineAgentHome } from "./home.js";
2
+ import type { WatchdogSqliteOwner } from "@fungi.computer/watchdog";
3
+ import { z } from "zod";
4
+ /** Public keys and selected identities only; a local store path never crosses. */
5
+ export declare const machineEnrollmentSchema: z.ZodObject<{
6
+ requestId: z.ZodString;
7
+ publicKey: z.ZodBase64;
8
+ machineLabel: z.ZodString;
9
+ agents: z.ZodArray<z.ZodObject<{
10
+ name: z.ZodString;
11
+ }, z.core.$strict>>;
12
+ }, z.core.$strict>;
13
+ export type MachineEnrollment = z.infer<typeof machineEnrollmentSchema>;
14
+ /** Trusted Local OAuth facts, never supplied in the public enrollment body. */
15
+ export declare const machineMemberSchema: z.ZodObject<{
16
+ memberId: z.ZodString;
17
+ clientId: z.ZodString;
18
+ loginId: z.ZodString;
19
+ }, z.core.$strict>;
20
+ export type MachineMember = z.infer<typeof machineMemberSchema>;
21
+ /** Server-selected issuer facts; omitted fields match all of this member's devices. */
22
+ export declare const machineLoginSelectionSchema: z.ZodObject<{
23
+ clientId: z.ZodOptional<z.ZodString>;
24
+ loginId: z.ZodOptional<z.ZodString>;
25
+ }, z.core.$strict>;
26
+ export type MachineLoginSelection = z.infer<typeof machineLoginSelectionSchema>;
27
+ /**
28
+ * Host-owned Agent identity reservation over the enrollment's own store. The
29
+ * same receipt yields the same Team Agent; no hosted Agent is opened.
30
+ */
31
+ export type MachineAgentReservation = (reservation: Readonly<{
32
+ memberId: string;
33
+ requestId: string;
34
+ name: string;
35
+ }>) => Readonly<{
36
+ agentId: string;
37
+ }>;
38
+ declare const limitsSchema: z.ZodObject<{
39
+ machines: z.ZodNumber;
40
+ agentsPerMachine: z.ZodNumber;
41
+ }, z.core.$strict>;
42
+ /** Honest enrollment refusals; none denotes command acceptance. */
43
+ export declare class MachineEnrollmentRefused extends Error {
44
+ readonly reason: "request_conflict" | "capacity" | "device_revoked" | "login_not_current";
45
+ constructor(reason: "request_conflict" | "capacity" | "device_revoked" | "login_not_current");
46
+ }
47
+ /**
48
+ * Team-owned enrollment state over the ordinary Agent directory, not a second
49
+ * hosted runtime. Its caller must supply the live, member-authorized identity.
50
+ * Limits are captured host policy; this module does not invent a Team quota.
51
+ */
52
+ export declare function createMachineEnrollment(options: {
53
+ owner: WatchdogSqliteOwner;
54
+ limits: z.infer<typeof limitsSchema>;
55
+ authorizeMember: (memberId: string) => Promise<void>;
56
+ authorizeLogin: (member: MachineMember) => Promise<boolean>;
57
+ onAgentCreated: (agentId: string) => void;
58
+ /** The host's Agent directory, opened over this enrollment's store. */
59
+ agents: (sql: WatchdogSqliteOwner["sql"]) => MachineAgentReservation;
60
+ }): {
61
+ enroll: (rawMember: MachineMember, raw: MachineEnrollment) => Promise<{
62
+ deviceId: string;
63
+ agents: {
64
+ selection: number;
65
+ agentId: string;
66
+ }[];
67
+ }>;
68
+ device: (deviceId: string) => {
69
+ deviceId: string;
70
+ memberId: string;
71
+ clientId: string;
72
+ loginId: string;
73
+ publicKey: string;
74
+ machineLabel: string;
75
+ fenceEpoch: number;
76
+ } | null;
77
+ home: (agentId: string) => MachineAgentHome | null;
78
+ revoke: (memberId: string, deviceId: string) => boolean;
79
+ authorizeDevice: (deviceId: string) => Promise<{
80
+ deviceId: string;
81
+ memberId: string;
82
+ clientId: string;
83
+ loginId: string;
84
+ publicKey: string;
85
+ machineLabel: string;
86
+ fenceEpoch: number;
87
+ }>;
88
+ retainedDevices: () => {
89
+ deviceId: string;
90
+ memberId: string;
91
+ clientId: string;
92
+ loginId: string;
93
+ revoked: boolean;
94
+ fenceEpoch: number;
95
+ }[];
96
+ fenceDevice: (deviceId: string) => number | null;
97
+ fenceMember: (memberId: string) => {
98
+ deviceId: string;
99
+ fenceEpoch: number;
100
+ }[];
101
+ fenceLogins: (memberId: string, selection: MachineLoginSelection) => {
102
+ deviceId: string;
103
+ fenceEpoch: number;
104
+ }[];
105
+ retireAllForErasure: () => {
106
+ deviceId: string;
107
+ fenceEpoch: number;
108
+ }[];
109
+ keyRetired: (deviceId: string) => boolean;
110
+ };
111
+ export {};