@fungi.computer/oyster 0.2.0 → 0.3.1
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.
- package/README.md +396 -72
- package/dist/agents.d.ts +3 -2
- package/dist/agents.js +4 -2
- package/dist/app-builds.d.ts +81 -8
- package/dist/app-builds.js +213 -60
- package/dist/app-storage.d.ts +3 -3
- package/dist/approval-capability.d.ts +5 -0
- package/dist/approval-capability.js +46 -0
- package/dist/approval-home.d.ts +51 -0
- package/dist/approval-home.js +226 -0
- package/dist/apps/catalog.d.ts +335 -0
- package/dist/apps/catalog.js +46 -0
- package/dist/apps/gate.d.ts +20 -6
- package/dist/apps/gate.js +42 -16
- package/dist/apps/index.d.ts +3 -1
- package/dist/apps/index.js +3 -1
- package/dist/apps/installation.d.ts +132 -51
- package/dist/apps/installation.js +176 -70
- package/dist/apps/permissions.d.ts +247 -0
- package/dist/apps/permissions.js +255 -0
- package/dist/boot-capability-route.d.ts +15 -0
- package/dist/boot-capability-route.js +196 -0
- package/dist/boot-capability.d.ts +103 -0
- package/dist/boot-capability.js +141 -0
- package/dist/boot-construction.d.ts +12 -0
- package/dist/boot-construction.js +175 -0
- package/dist/boot-gate.d.ts +267 -0
- package/dist/boot-gate.js +236 -0
- package/dist/boot-host-script.d.ts +2 -0
- package/dist/boot-host-script.js +1 -0
- package/dist/boot-mount.d.ts +21 -0
- package/dist/boot-mount.js +65 -0
- package/dist/browser/boot.d.ts +73 -0
- package/dist/browser/boot.js +502 -0
- package/dist/browser/browser.d.ts +104 -0
- package/dist/browser/browser.js +374 -0
- package/dist/browser/capability-source.d.ts +76 -0
- package/dist/browser/capability-source.js +338 -0
- package/dist/browser/cdp-client.d.ts +56 -0
- package/dist/browser/cdp-client.js +184 -0
- package/dist/browser/errors.d.ts +79 -0
- package/dist/browser/errors.js +32 -0
- package/dist/browser/index.d.ts +16 -0
- package/dist/browser/index.js +14 -0
- package/dist/browser/lease-registry.d.ts +61 -0
- package/dist/browser/lease-registry.js +101 -0
- package/dist/browser/operations.d.ts +225 -0
- package/dist/browser/operations.js +137 -0
- package/dist/browser/page.d.ts +25 -0
- package/dist/browser/page.js +172 -0
- package/dist/browser/quick-reads.d.ts +29 -0
- package/dist/browser/quick-reads.js +160 -0
- package/dist/browser/sessions.d.ts +52 -0
- package/dist/browser/sessions.js +118 -0
- package/dist/browser/url-policy.d.ts +18 -0
- package/dist/browser/url-policy.js +58 -0
- package/dist/build/app-imports.d.ts +12 -0
- package/dist/build/app-imports.js +105 -0
- package/dist/build/build-policy.d.ts +5 -3
- package/dist/build/build-policy.js +5 -3
- package/dist/build/candidate-builder.d.ts +1 -1
- package/dist/build/candidate-cli.mjs +8375 -334
- package/dist/build/local-build.d.ts +2 -0
- package/dist/build/local-build.js +28 -6
- package/dist/build/snapshot-package-store.d.ts +1 -1
- package/dist/build/snapshot-package-store.js +7 -2
- package/dist/build/workspace-build.d.ts +92 -5
- package/dist/build/workspace-build.js +93 -19
- package/dist/build.d.ts +1 -1
- package/dist/build.js +1 -1
- package/dist/catalog-frame.js +1 -1
- package/dist/cloudflare.d.ts +13 -3
- package/dist/cloudflare.js +102 -4
- package/dist/frame-binding-wire-script.js +1 -1
- package/dist/frame-binding-wire.js +4 -2
- package/dist/git-auth.d.ts +0 -2
- package/dist/git-auth.js +0 -2
- package/dist/git.d.ts +0 -1
- package/dist/git.js +0 -2
- package/dist/host.d.ts +39 -13
- package/dist/host.js +268 -142
- package/dist/index.d.ts +25 -15
- package/dist/index.js +39 -16
- package/dist/install-release-cli.d.ts +2 -0
- package/dist/install-release-cli.js +46 -0
- package/dist/installation-client.d.ts +43 -0
- package/dist/installation-client.js +178 -0
- package/dist/installation-execution.d.ts +11 -0
- package/dist/installation-execution.js +184 -0
- package/dist/installation-operations.d.ts +1437 -0
- package/dist/installation-operations.js +664 -0
- package/dist/installation-store-generation.d.ts +3 -0
- package/dist/installation-store-generation.js +16 -0
- package/dist/instance-id.d.ts +10 -0
- package/dist/instance-id.js +10 -0
- package/dist/instance-refusal.d.ts +11 -0
- package/dist/instance-refusal.js +21 -0
- package/dist/internal/delivery.js +4 -2
- package/dist/internal/host-gate.js +7 -1
- package/dist/internal/registry-installations.js +4 -4
- package/dist/keys.d.ts +5 -1
- package/dist/keys.js +10 -2
- package/dist/machine-link/authority.d.ts +89 -0
- package/dist/machine-link/authority.js +37 -0
- package/dist/machine-link/client.d.ts +66 -0
- package/dist/machine-link/client.js +94 -0
- package/dist/machine-link/delivery.d.ts +18 -0
- package/dist/machine-link/delivery.js +55 -0
- package/dist/machine-link/enrollment.d.ts +111 -0
- package/dist/machine-link/enrollment.js +286 -0
- package/dist/machine-link/fences.d.ts +37 -0
- package/dist/machine-link/fences.js +166 -0
- package/dist/machine-link/home.d.ts +20 -0
- package/dist/machine-link/home.js +24 -0
- package/dist/machine-link/host.d.ts +43 -0
- package/dist/machine-link/host.js +189 -0
- package/dist/machine-link/index.d.ts +15 -0
- package/dist/machine-link/index.js +15 -0
- package/dist/machine-link/link.d.ts +48 -0
- package/dist/machine-link/link.js +223 -0
- package/dist/machine-link/presence.d.ts +10 -0
- package/dist/machine-link/presence.js +25 -0
- package/dist/machine-link/workers.d.ts +6 -0
- package/dist/machine-link/workers.js +6 -0
- package/dist/mcp-owner.d.ts +2 -2
- package/dist/mcp-owner.js +41 -33
- package/dist/mcp.d.ts +15 -3
- package/dist/mcp.js +146 -25
- package/dist/older-oyster-store-refusal.d.ts +4 -0
- package/dist/older-oyster-store-refusal.js +18 -0
- package/dist/older-oyster-store.d.ts +21 -0
- package/dist/older-oyster-store.js +43 -0
- package/dist/pack.d.ts +14 -3
- package/dist/pack.js +44 -19
- package/dist/page/app-access.d.ts +7 -0
- package/dist/page/app-access.js +75 -0
- package/dist/page/boot-host.js +57 -0
- package/dist/page/capabilities.d.ts +3 -3
- package/dist/page/capabilities.js +16 -12
- package/dist/page/computer-handles.d.ts +3 -3
- package/dist/page/computer-target.d.ts +3 -0
- package/dist/page/computer-target.js +9 -0
- package/dist/page/mount.d.ts +23 -3
- package/dist/page/mount.js +152 -16
- package/dist/page-assets.d.ts +22 -0
- package/dist/page-assets.js +28 -0
- package/dist/page-script.d.ts +3 -0
- package/dist/page-script.js +5 -4
- package/dist/release.d.ts +13 -6
- package/dist/release.js +10 -7
- package/dist/root-data.d.ts +16 -6
- package/dist/root-data.js +2 -2
- package/dist/source/index.d.ts +1 -1
- package/dist/source/team-source.d.ts +20 -8
- package/dist/source/team-source.js +39 -10
- package/dist/source/template-seed.d.ts +2 -2
- package/dist/standalone/agent-mail.d.ts +24 -0
- package/dist/standalone/agent-mail.js +62 -0
- package/dist/standalone/agent-store.d.ts +9 -0
- package/dist/standalone/agent-store.js +30 -0
- package/dist/standalone/agents.d.ts +68 -9
- package/dist/standalone/agents.js +428 -56
- package/dist/standalone/app-builds-binding.d.ts +20 -2
- package/dist/standalone/app-builds-binding.js +147 -23
- package/dist/standalone/app-set.d.ts +5 -2
- package/dist/standalone/app-set.js +186 -144
- package/dist/standalone/browser.d.ts +11 -0
- package/dist/standalone/browser.js +72 -0
- package/dist/standalone/builds.d.ts +13 -4
- package/dist/standalone/builds.js +28 -44
- package/dist/standalone/computer-egress.d.ts +2 -0
- package/dist/standalone/computer-egress.js +13 -4
- package/dist/standalone/computer-fleet.d.ts +43 -0
- package/dist/standalone/computer-fleet.js +200 -0
- package/dist/standalone/computer.d.ts +44 -9
- package/dist/standalone/computer.js +129 -22
- package/dist/standalone/install-apps-cli.js +43 -6
- package/dist/standalone/instance.d.ts +2 -2
- package/dist/standalone/instance.js +8 -11
- package/dist/standalone/machine-link.d.ts +354 -0
- package/dist/standalone/machine-link.js +368 -0
- package/dist/standalone/media.d.ts +2 -2
- package/dist/standalone/media.js +19 -16
- package/dist/standalone/preview.d.ts +2 -2
- package/dist/standalone/preview.js +37 -10
- package/dist/standalone/registry.d.ts +151 -9
- package/dist/standalone/registry.js +305 -8
- package/dist/standalone/worker.d.ts +19 -5
- package/dist/standalone/worker.js +90 -54
- package/dist/standalone/workflow-binding.d.ts +18 -22
- package/dist/standalone/workflow-binding.js +18 -18
- package/dist/standalone/workflow-control.d.ts +4 -0
- package/dist/standalone/workflow-control.js +40 -0
- package/dist/standalone/workflow-events.d.ts +2 -2
- package/dist/standalone/workflow-events.js +9 -10
- package/dist/standalone/workflow-git-capture.js +4 -3
- package/dist/standalone/workflow-host.d.ts +162 -161
- package/dist/standalone/workflow-host.js +101 -21
- package/dist/standalone/workflow-node-capture.js +11 -4
- package/dist/standalone/workflow-schedules.d.ts +5 -2
- package/dist/standalone/workflow-schedules.js +9 -10
- package/dist/standalone/workflow-trigger-host.d.ts +65 -5
- package/dist/standalone/workflow-trigger-host.js +2 -2
- package/dist/standalone/workflow-webhooks.d.ts +22 -0
- package/dist/standalone/workflow-webhooks.js +39 -0
- package/dist/standalone/workflows.d.ts +140 -43
- package/dist/standalone/workflows.js +198 -39
- package/dist/storage-frame.d.ts +6 -6
- package/dist/testing/capabilities.js +14 -3
- package/dist/testing/installation.js +26 -2
- package/dist/testing/worker.js +1 -1
- package/dist/testing/workers-runtime.d.ts +8 -2
- package/dist/testing/workers-runtime.js +20 -2
- package/dist/webhook-abuse.d.ts +25 -0
- package/dist/webhook-abuse.js +54 -0
- package/dist/webhook-custody.d.ts +144 -0
- package/dist/webhook-custody.js +222 -0
- package/dist/webhook-ingress.d.ts +36 -0
- package/dist/webhook-ingress.js +146 -0
- package/dist/webhooks.d.ts +111 -0
- package/dist/webhooks.js +189 -0
- package/dist/workers-git.d.ts +12 -17
- package/dist/workers-git.js +64 -52
- package/dist/workers.d.ts +226 -13
- package/dist/workers.js +363 -51
- package/dist/workflow-agent-authority.d.ts +8 -0
- package/dist/workflow-agent-authority.js +42 -0
- package/dist/workflows.d.ts +66 -0
- package/dist/workflows.js +97 -0
- package/docs/api/README.md +63 -37
- package/docs/api/agents.md +17 -0
- package/docs/api/app-storage.md +1 -1
- package/docs/api/apps.md +1394 -543
- package/docs/api/browser/boot.md +132 -0
- package/docs/api/browser.md +1440 -0
- package/docs/api/build-contract.md +38 -25
- package/docs/api/build.md +263 -84
- package/docs/api/cloudflare.md +93 -7
- package/docs/api/git.md +3 -13
- package/docs/api/host.md +944 -118
- package/docs/api/index.md +320 -18
- package/docs/api/keys.md +24 -1
- package/docs/api/mcp.md +59 -12
- package/docs/api/modules.md +33 -40
- package/docs/api/pack.md +57 -8
- package/docs/api/page/mount.md +92 -4
- package/docs/api/workers-git.md +103 -24
- package/docs/api/workers.md +1594 -244
- package/docs/site/agent-quickstart.mdx +28 -14
- package/examples/standalone/worker.ts +3 -1
- package/examples/standalone/wrangler.jsonc +2 -0
- package/package.json +73 -71
- package/dist/installation-consent.d.ts +0 -81
- package/dist/installation-consent.js +0 -84
- package/dist/shell/app-directory.d.ts +0 -542
- package/dist/shell/app-directory.js +0 -196
- package/dist/shell/browser-actions.d.ts +0 -7
- package/dist/shell/browser-actions.js +0 -56
- package/dist/shell/command-host.d.ts +0 -112
- package/dist/shell/command-host.js +0 -502
- package/dist/shell/commands.d.ts +0 -25
- package/dist/shell/commands.js +0 -78
- package/dist/shell/index.d.ts +0 -8
- package/dist/shell/index.js +0 -8
- package/dist/shell/keys.d.ts +0 -41
- package/dist/shell/keys.js +0 -134
- package/dist/shell/shell-module.d.ts +0 -55
- package/dist/shell/shell-module.js +0 -86
- package/dist/shell/window-manager.d.ts +0 -128
- package/dist/shell/window-manager.js +0 -730
- package/dist/shell/workbench-contract.d.ts +0 -68
- package/dist/shell/workbench-directory.d.ts +0 -332
- package/dist/shell/workbench-directory.js +0 -35
- package/dist/shell/workbench-host-types.d.ts +0 -96
- package/dist/shell/workbench-host-types.js +0 -1
- package/dist/shell/workbench-host.d.ts +0 -34
- package/dist/shell/workbench-host.js +0 -352
- package/dist/shell/workbench-model.d.ts +0 -194
- package/dist/shell/workbench-model.js +0 -53
- package/dist/shell/workbench.d.ts +0 -4
- package/dist/shell/workbench.js +0 -2
- package/docs/api/shell/app-directory.md +0 -599
- package/docs/api/shell/command-host.md +0 -224
- package/docs/api/shell/commands.md +0 -197
- package/docs/api/shell/keys.md +0 -183
- package/docs/api/shell/shell-module.md +0 -204
- package/docs/api/shell/window-manager.md +0 -267
- package/docs/api/shell/workbench-directory.md +0 -119
- package/docs/api/shell/workbench.md +0 -205
- package/docs/api/shell.md +0 -1915
- /package/dist/{shell/workbench-contract.js → page/boot-host.d.ts} +0 -0
|
@@ -0,0 +1,368 @@
|
|
|
1
|
+
import { machineAgentReservations } from "@fungi.computer/shiitake/delegation";
|
|
2
|
+
import { createMachineEnrollment, MachineEnrollmentRefused, MachineLinkRefused, } from "../machine-link/index.js";
|
|
3
|
+
import { keyAllowsMachineEnroll, } from "../keys.js";
|
|
4
|
+
import { agentIds, createAgentDirectory, } from "@fungi.computer/shiitake/fleet";
|
|
5
|
+
import { DurableObject } from "cloudflare:workers";
|
|
6
|
+
import { Hono } from "hono";
|
|
7
|
+
import { bodyLimit } from "hono/body-limit";
|
|
8
|
+
import { validator } from "hono/validator";
|
|
9
|
+
import { machineEnrollmentSchema, machineLinkName, } from "../machine-link/index.js";
|
|
10
|
+
import { createMachineLink } from "../machine-link/workers.js";
|
|
11
|
+
import { defaultTenant, tenantScope } from "../tenant.js";
|
|
12
|
+
import { presentedGitKey } from "../git-auth.js";
|
|
13
|
+
import { standaloneTenancy } from "./tenancy.js";
|
|
14
|
+
/** Standalone's fresh directory projection for the shared hosted client. */
|
|
15
|
+
export function standaloneMachineAgentHome(options) {
|
|
16
|
+
return async (agentId) => {
|
|
17
|
+
const actor = options.identity();
|
|
18
|
+
if (actor === null)
|
|
19
|
+
throw new MachineLinkRefused("actor_not_allowed");
|
|
20
|
+
const directory = options.directory.getByName(actor.teamId);
|
|
21
|
+
const home = await directory.agentHome(agentId);
|
|
22
|
+
if (home === null)
|
|
23
|
+
throw new MachineLinkRefused("actor_not_allowed");
|
|
24
|
+
if (home.kind === "hosted")
|
|
25
|
+
return home;
|
|
26
|
+
// Admit the source and live device before the client addresses its link.
|
|
27
|
+
return (await directory.machineTarget({ kind: "hosted", agentId: actor.agentId }, agentId, null)).targetHome;
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
/** Machine-link teamId is the existing owner's Agent directory scope. */
|
|
31
|
+
const scopeFor = (env) => tenantScope(defaultTenant, env.OYSTER_ORIGIN);
|
|
32
|
+
async function liveOwner(env) {
|
|
33
|
+
const tenant = await env.TENANTS.getByName("registry").get(defaultTenant);
|
|
34
|
+
// A mesh request cannot provision a missing owner scope or cold-start
|
|
35
|
+
// tenancy's seed path while that scope is held inactive.
|
|
36
|
+
if (tenant?.status !== "active")
|
|
37
|
+
return null;
|
|
38
|
+
return (await standaloneTenancy(env)).owner();
|
|
39
|
+
}
|
|
40
|
+
/** Fresh host authority; no answer is retained by the adapter or the link. */
|
|
41
|
+
export function standaloneMachineLinkAuthority(env) {
|
|
42
|
+
const scope = scopeFor(env);
|
|
43
|
+
const teamId = scope.durableName("agents");
|
|
44
|
+
const directory = () => env.AGENT_DIRECTORY.getByName(teamId);
|
|
45
|
+
const authority = {
|
|
46
|
+
admitDevice: async (id) => {
|
|
47
|
+
if (id.teamId !== teamId || (await liveOwner(env)) === null)
|
|
48
|
+
return { live: false };
|
|
49
|
+
return directory().machineDevice(id);
|
|
50
|
+
},
|
|
51
|
+
currentFence: async (id) => id.teamId !== teamId ? null : directory().machineFence(id),
|
|
52
|
+
directory: async (id) => {
|
|
53
|
+
if (!(await authority.admitDevice(id)).live)
|
|
54
|
+
throw new MachineLinkRefused("actor_not_allowed");
|
|
55
|
+
return directory().machineDirectory(id);
|
|
56
|
+
},
|
|
57
|
+
resolveTarget: async (id, caller, agentId, requester) => {
|
|
58
|
+
if (!(await authority.admitDevice(id)).live)
|
|
59
|
+
throw new MachineLinkRefused("actor_not_allowed");
|
|
60
|
+
return directory().machineTarget(caller, agentId, requester);
|
|
61
|
+
},
|
|
62
|
+
deliverHome: async (id, caller, target, envelope) => {
|
|
63
|
+
// The projection used to prepare this delivery lends no later authority.
|
|
64
|
+
const facts = await authority.resolveTarget(id, caller, target.targetAgentId, null);
|
|
65
|
+
if (facts.targetHome.kind === "hosted")
|
|
66
|
+
return env.AGENTS.getByName(facts.targetAgentId).deliver(envelope);
|
|
67
|
+
const next = { teamId, deviceId: facts.targetHome.deviceId };
|
|
68
|
+
if (!(await authority.admitDevice(next)).live)
|
|
69
|
+
throw new MachineLinkRefused("actor_not_allowed");
|
|
70
|
+
return env.MACHINE_LINK.getByName(machineLinkName(next)).deliver(next, caller, facts.targetAgentId, envelope);
|
|
71
|
+
},
|
|
72
|
+
canErase: async (id) => {
|
|
73
|
+
if (id.teamId !== teamId)
|
|
74
|
+
return false;
|
|
75
|
+
const tenant = await env.TENANTS.getByName("registry").get(defaultTenant);
|
|
76
|
+
// Losing a login or a failed owner read is not erasure permission.
|
|
77
|
+
// Like the Hub: the owner scope is held inactive AND the device key is
|
|
78
|
+
// irreversibly retired in its retained enrollment owner.
|
|
79
|
+
return (tenant !== null &&
|
|
80
|
+
tenant.origin === scope.origin &&
|
|
81
|
+
(tenant.status === "suspended" || tenant.status === "deleting") &&
|
|
82
|
+
(await directory().machineKeyRetired(id.deviceId)));
|
|
83
|
+
},
|
|
84
|
+
deliverControlFact: async (fact) => {
|
|
85
|
+
const tenant = await env.TENANTS.getByName("registry").get(defaultTenant);
|
|
86
|
+
if (fact.teamId !== teamId ||
|
|
87
|
+
tenant?.origin !== scope.origin ||
|
|
88
|
+
(await authority.currentFence(fact)) === null)
|
|
89
|
+
throw new MachineLinkRefused("actor_not_allowed");
|
|
90
|
+
return env.MACHINE_LINK.getByName(machineLinkName(fact)).applyFence(fact);
|
|
91
|
+
},
|
|
92
|
+
};
|
|
93
|
+
return authority;
|
|
94
|
+
}
|
|
95
|
+
/** HTTP enrollment over the existing live KEYS and directory owners. */
|
|
96
|
+
export function standaloneMachineJoinRoutes(options) {
|
|
97
|
+
const { scope } = options;
|
|
98
|
+
const teamId = scope.durableName("agents");
|
|
99
|
+
return new Hono().post("/local/mesh/join", bodyLimit({ maxSize: 512 * 1024 }), validator("json", (value, context) => {
|
|
100
|
+
const parsed = machineEnrollmentSchema.safeParse(value);
|
|
101
|
+
return parsed.success
|
|
102
|
+
? parsed.data
|
|
103
|
+
: context.json({ error: "invalid_enrollment" }, 400);
|
|
104
|
+
}), async (context) => {
|
|
105
|
+
if (new URL(context.req.url).origin !== scope.origin)
|
|
106
|
+
return context.body(null, 403);
|
|
107
|
+
const ownerId = await options.ownerId();
|
|
108
|
+
if (ownerId === null)
|
|
109
|
+
return context.body(null, 403);
|
|
110
|
+
const presented = presentedGitKey(context.req.raw);
|
|
111
|
+
if (presented.kind !== "key")
|
|
112
|
+
return context.body(null, 401);
|
|
113
|
+
const keys = options.keys();
|
|
114
|
+
const verified = await keys.verify(presented.value);
|
|
115
|
+
if (verified.kind !== "ok")
|
|
116
|
+
return context.body(null, 401);
|
|
117
|
+
const live = await keys.live(verified.value.keyId);
|
|
118
|
+
if (live?.owner !== ownerId || !keyAllowsMachineEnroll(live))
|
|
119
|
+
return context.body(null, 403);
|
|
120
|
+
const joined = await options.enroll(live.keyId, context.req.valid("json"));
|
|
121
|
+
// Domain refusals cross RPC as data, not an instanceof claim about a
|
|
122
|
+
// platform-reconstructed Error.
|
|
123
|
+
if (joined.kind === "refused") {
|
|
124
|
+
if (joined.error === "request_conflict" || joined.error === "capacity")
|
|
125
|
+
return context.json({ error: joined.error }, 409);
|
|
126
|
+
if (joined.error === "invalid_enrollment")
|
|
127
|
+
return context.json({ error: "invalid_enrollment" }, 400);
|
|
128
|
+
return context.body(null, 403);
|
|
129
|
+
}
|
|
130
|
+
return context.json({ kind: "joined", scopeId: teamId, ...joined.value }, 200);
|
|
131
|
+
});
|
|
132
|
+
}
|
|
133
|
+
/** Only join accepts an oyk credential; every subsequent dial uses Seam. */
|
|
134
|
+
export function standaloneMachineLinkRoutes(env) {
|
|
135
|
+
const scope = scopeFor(env);
|
|
136
|
+
const teamId = scope.durableName("agents");
|
|
137
|
+
return standaloneMachineJoinRoutes({
|
|
138
|
+
scope,
|
|
139
|
+
ownerId: async () => (await liveOwner(env))?.actor.memberId ?? null,
|
|
140
|
+
keys: () => env.KEYS.getByName(scope.durableName("keys")),
|
|
141
|
+
enroll: (keyId, input) => env.AGENT_DIRECTORY.getByName(teamId).enrollMachine(keyId, input),
|
|
142
|
+
}).get("/api/teams/:teamId/local/mesh/:deviceId/connect", async (context) => {
|
|
143
|
+
const id = {
|
|
144
|
+
teamId: context.req.param("teamId"),
|
|
145
|
+
deviceId: context.req.param("deviceId"),
|
|
146
|
+
};
|
|
147
|
+
if (new URL(context.req.url).origin !== scope.origin ||
|
|
148
|
+
id.teamId !== teamId ||
|
|
149
|
+
!(await standaloneMachineLinkAuthority(env).admitDevice(id)).live)
|
|
150
|
+
return context.body(null, 403);
|
|
151
|
+
// F1: never address an unknown scope/device, even for a refused dial.
|
|
152
|
+
// The shared link independently rereads admission and verifies Seam.
|
|
153
|
+
return env.MACHINE_LINK.getByName(machineLinkName(id)).fetch(context.req.raw);
|
|
154
|
+
});
|
|
155
|
+
}
|
|
156
|
+
const erasedKey = "machine-link:erased";
|
|
157
|
+
/** One device's link; directory, credentials and owner state remain elsewhere. */
|
|
158
|
+
export class MachineLinkDO extends DurableObject {
|
|
159
|
+
link = createMachineLink(this.ctx, {
|
|
160
|
+
authority: standaloneMachineLinkAuthority(this.env),
|
|
161
|
+
addressed: (identity) => this.ctx.id.equals(this.env.MACHINE_LINK.idFromName(machineLinkName(identity))),
|
|
162
|
+
erasure: {
|
|
163
|
+
erased: async () => (await this.ctx.storage.get(erasedKey)) === true,
|
|
164
|
+
read: async () => ({
|
|
165
|
+
erased: (await this.ctx.storage.get(erasedKey)) === true,
|
|
166
|
+
}),
|
|
167
|
+
erase: async () => {
|
|
168
|
+
// Only this retired device's socket/replay owner, never an Agent,
|
|
169
|
+
// directory or KEYS store. The shared factory checks canErase first.
|
|
170
|
+
await this.ctx.storage.deleteAll();
|
|
171
|
+
await this.ctx.storage.put(erasedKey, true);
|
|
172
|
+
return { erased: true };
|
|
173
|
+
},
|
|
174
|
+
},
|
|
175
|
+
report: (operation, cause) => {
|
|
176
|
+
console.error(operation, cause);
|
|
177
|
+
},
|
|
178
|
+
});
|
|
179
|
+
fetch(request) {
|
|
180
|
+
return this.link.fetch(request);
|
|
181
|
+
}
|
|
182
|
+
deliver(id, caller, agentId, envelope) {
|
|
183
|
+
return this.link.deliver(id, caller, agentId, envelope);
|
|
184
|
+
}
|
|
185
|
+
operation(id, caller, agentId, operation) {
|
|
186
|
+
return this.link.operation(id, caller, agentId, operation);
|
|
187
|
+
}
|
|
188
|
+
applyFence(fact) {
|
|
189
|
+
return Promise.resolve(this.link.applyFence(fact));
|
|
190
|
+
}
|
|
191
|
+
preflightErasure(teamId, deviceId) {
|
|
192
|
+
return this.link.preflightErasure(teamId, deviceId);
|
|
193
|
+
}
|
|
194
|
+
eraseForTeam(teamId, deviceId) {
|
|
195
|
+
return this.link.eraseForTeam(teamId, deviceId);
|
|
196
|
+
}
|
|
197
|
+
readErasure() {
|
|
198
|
+
return this.link.readErasure();
|
|
199
|
+
}
|
|
200
|
+
webSocketMessage(socket, message) {
|
|
201
|
+
return this.link.webSocketMessage(socket, message);
|
|
202
|
+
}
|
|
203
|
+
webSocketClose(socket, code, reason) {
|
|
204
|
+
this.link.webSocketClose(socket, code, reason);
|
|
205
|
+
}
|
|
206
|
+
webSocketError(socket) {
|
|
207
|
+
this.link.webSocketError(socket);
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
/** Enrollment and the ordinary directory share this one SQLite owner. */
|
|
211
|
+
export function createStandaloneMachineEnrollment(options) {
|
|
212
|
+
options.owner.sql.exec("CREATE TABLE IF NOT EXISTS standalone_machine_enroll_keys (key_id TEXT PRIMARY KEY, request_id TEXT NOT NULL) STRICT");
|
|
213
|
+
const configured = {
|
|
214
|
+
// Retained host's private storage/representation ceilings, not plan quotas.
|
|
215
|
+
limits: { machines: 256, agentsPerMachine: 256 },
|
|
216
|
+
authorizeMember: async (memberId) => {
|
|
217
|
+
if ((await options.ownerId()) !== memberId)
|
|
218
|
+
throw new MachineLinkRefused("actor_not_allowed");
|
|
219
|
+
},
|
|
220
|
+
// Enrollment keys buy identity once; they are not ongoing device credentials.
|
|
221
|
+
authorizeLogin: async ({ memberId }) => (await options.ownerId()) === memberId,
|
|
222
|
+
onAgentCreated: options.onAgentCreated,
|
|
223
|
+
agents: machineAgentReservations,
|
|
224
|
+
};
|
|
225
|
+
const enrollment = createMachineEnrollment({
|
|
226
|
+
...configured,
|
|
227
|
+
owner: options.owner,
|
|
228
|
+
});
|
|
229
|
+
// A view of the ordinary directory in this same store, not another catalog.
|
|
230
|
+
const directory = createAgentDirectory(options.owner.sql);
|
|
231
|
+
const grant = (memberId) => ({
|
|
232
|
+
agents: "all",
|
|
233
|
+
actions: ["read"],
|
|
234
|
+
expiresAt: Date.now() + 60_000,
|
|
235
|
+
provenance: { actor: memberId },
|
|
236
|
+
});
|
|
237
|
+
const owner = async () => {
|
|
238
|
+
const memberId = await options.ownerId();
|
|
239
|
+
if (memberId === null)
|
|
240
|
+
throw new MachineLinkRefused("actor_not_allowed");
|
|
241
|
+
return memberId;
|
|
242
|
+
};
|
|
243
|
+
const admitDevice = async (id) => {
|
|
244
|
+
if (id.teamId !== options.scopeId || (await options.ownerId()) === null)
|
|
245
|
+
return { live: false };
|
|
246
|
+
try {
|
|
247
|
+
const device = await enrollment.authorizeDevice(id.deviceId);
|
|
248
|
+
return {
|
|
249
|
+
live: true,
|
|
250
|
+
teamId: options.scopeId,
|
|
251
|
+
deviceId: device.deviceId,
|
|
252
|
+
memberId: device.memberId,
|
|
253
|
+
publicKey: device.publicKey,
|
|
254
|
+
fenceEpoch: device.fenceEpoch,
|
|
255
|
+
};
|
|
256
|
+
}
|
|
257
|
+
catch (cause) {
|
|
258
|
+
if (cause instanceof MachineEnrollmentRefused ||
|
|
259
|
+
cause instanceof MachineLinkRefused)
|
|
260
|
+
return { live: false };
|
|
261
|
+
throw cause;
|
|
262
|
+
}
|
|
263
|
+
};
|
|
264
|
+
const currentFence = async (id) => {
|
|
265
|
+
if (id.teamId !== options.scopeId)
|
|
266
|
+
return null;
|
|
267
|
+
const memberId = await options.ownerId();
|
|
268
|
+
const device = enrollment
|
|
269
|
+
.retainedDevices()
|
|
270
|
+
.find((row) => row.deviceId === id.deviceId);
|
|
271
|
+
return device === undefined
|
|
272
|
+
? null
|
|
273
|
+
: {
|
|
274
|
+
...id,
|
|
275
|
+
fenceEpoch: device.fenceEpoch,
|
|
276
|
+
live: memberId === device.memberId && !device.revoked,
|
|
277
|
+
};
|
|
278
|
+
};
|
|
279
|
+
const target = async (caller, agentId, requester) => {
|
|
280
|
+
const memberId = await owner();
|
|
281
|
+
const exists = (id) => {
|
|
282
|
+
const parsed = agentIds.agent.parse(id);
|
|
283
|
+
return (parsed.ok && directory.read(grant(memberId), parsed.value) !== null);
|
|
284
|
+
};
|
|
285
|
+
if (!exists(caller.agentId) ||
|
|
286
|
+
!exists(agentId) ||
|
|
287
|
+
(requester !== null &&
|
|
288
|
+
(!exists(requester) ||
|
|
289
|
+
(caller.agentId !== agentId && caller.agentId !== requester))))
|
|
290
|
+
throw new MachineLinkRefused("actor_not_allowed");
|
|
291
|
+
const source = enrollment.home(caller.agentId);
|
|
292
|
+
if (caller.kind === "device"
|
|
293
|
+
? source?.kind !== "machine" || source.deviceId !== caller.deviceId
|
|
294
|
+
: source !== null)
|
|
295
|
+
throw new MachineLinkRefused("actor_not_allowed");
|
|
296
|
+
if (source?.kind === "machine")
|
|
297
|
+
await enrollment.authorizeDevice(source.deviceId);
|
|
298
|
+
const targetHome = enrollment.home(agentId) ?? {
|
|
299
|
+
kind: "hosted",
|
|
300
|
+
};
|
|
301
|
+
if (targetHome.kind === "machine")
|
|
302
|
+
await enrollment.authorizeDevice(targetHome.deviceId);
|
|
303
|
+
return {
|
|
304
|
+
teamId: options.scopeId,
|
|
305
|
+
callerAgentId: caller.agentId,
|
|
306
|
+
targetAgentId: agentId,
|
|
307
|
+
actorAgentId: requester ?? caller.agentId,
|
|
308
|
+
callerHome: source ?? { kind: "hosted" },
|
|
309
|
+
targetHome,
|
|
310
|
+
};
|
|
311
|
+
};
|
|
312
|
+
return {
|
|
313
|
+
...enrollment,
|
|
314
|
+
admitDevice,
|
|
315
|
+
currentFence,
|
|
316
|
+
target,
|
|
317
|
+
async publicDirectory(id) {
|
|
318
|
+
if (!(await admitDevice(id)).live)
|
|
319
|
+
throw new MachineLinkRefused("actor_not_allowed");
|
|
320
|
+
const memberId = await owner();
|
|
321
|
+
return directory
|
|
322
|
+
.list(grant(memberId))
|
|
323
|
+
.map(({ agentId, name }) => ({ agentId, name }));
|
|
324
|
+
},
|
|
325
|
+
async enrollWithKey(keyId, input) {
|
|
326
|
+
const memberId = await options.ownerId();
|
|
327
|
+
const key = await options.liveKey(keyId);
|
|
328
|
+
if (memberId === null ||
|
|
329
|
+
key?.owner !== memberId ||
|
|
330
|
+
!keyAllowsMachineEnroll(key))
|
|
331
|
+
throw new MachineLinkRefused("actor_not_allowed");
|
|
332
|
+
// This wrapper is per call: concurrent joins never share a mutable key
|
|
333
|
+
// slot. createMachineEnrollment invokes it only around its synchronous
|
|
334
|
+
// identity/directory commit, after awaited validation and authority reads.
|
|
335
|
+
const consumingOwner = {
|
|
336
|
+
sql: options.owner.sql,
|
|
337
|
+
transactionSync: (body) => options.owner.transactionSync(() => {
|
|
338
|
+
const consumed = options.owner.sql
|
|
339
|
+
.exec("SELECT key_id FROM standalone_machine_enroll_keys WHERE key_id=?", keyId)
|
|
340
|
+
.toArray().length !== 0;
|
|
341
|
+
if (consumed) {
|
|
342
|
+
// Replay is an ordinary enrollment receipt, not another spend.
|
|
343
|
+
// The shared owner checks canonical body, member, login and
|
|
344
|
+
// retirement before returning its original directory identities.
|
|
345
|
+
if (options.owner.sql
|
|
346
|
+
.exec("SELECT 1 FROM standalone_machine_enroll_keys WHERE key_id=? AND request_id=?", keyId, input.requestId)
|
|
347
|
+
.toArray().length === 0)
|
|
348
|
+
throw new MachineLinkRefused("actor_not_allowed");
|
|
349
|
+
}
|
|
350
|
+
else {
|
|
351
|
+
options.owner.sql.exec("INSERT INTO standalone_machine_enroll_keys (key_id, request_id) VALUES (?, ?)", keyId, input.requestId);
|
|
352
|
+
}
|
|
353
|
+
return body();
|
|
354
|
+
}),
|
|
355
|
+
};
|
|
356
|
+
return createMachineEnrollment({
|
|
357
|
+
...configured,
|
|
358
|
+
owner: consumingOwner,
|
|
359
|
+
authorizeMember: async (member) => {
|
|
360
|
+
await configured.authorizeMember(member);
|
|
361
|
+
const current = await options.liveKey(keyId);
|
|
362
|
+
if (current?.owner !== member || !keyAllowsMachineEnroll(current))
|
|
363
|
+
throw new MachineLinkRefused("actor_not_allowed");
|
|
364
|
+
},
|
|
365
|
+
}).enroll({ memberId, clientId: "oyster-key", loginId: key.keyId }, input);
|
|
366
|
+
},
|
|
367
|
+
};
|
|
368
|
+
}
|
|
@@ -41,7 +41,7 @@ export declare const mediaFrameMethods: {
|
|
|
41
41
|
aspectRatio?: "16:9" | "4:3" | "1:1" | "3:4" | "9:16" | "21:9" | "9:21" | undefined;
|
|
42
42
|
references?: {
|
|
43
43
|
path: string;
|
|
44
|
-
role?: "
|
|
44
|
+
role?: "reference" | "first-frame" | "last-frame" | undefined;
|
|
45
45
|
}[] | undefined;
|
|
46
46
|
generateAudio?: boolean | undefined;
|
|
47
47
|
steps?: number | undefined;
|
|
@@ -60,7 +60,7 @@ export declare const mediaFrameMethods: {
|
|
|
60
60
|
generateAudio?: boolean | undefined;
|
|
61
61
|
references?: {
|
|
62
62
|
path: string;
|
|
63
|
-
role: "
|
|
63
|
+
role: "reference" | "first-frame" | "last-frame";
|
|
64
64
|
}[] | undefined;
|
|
65
65
|
};
|
|
66
66
|
}];
|
package/dist/standalone/media.js
CHANGED
|
@@ -53,21 +53,24 @@ export function workersMedia(env, workspace) {
|
|
|
53
53
|
});
|
|
54
54
|
if (!response.ok) {
|
|
55
55
|
const failure = await readMediaFailure(response);
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
:
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
56
|
+
const projected = failure === null
|
|
57
|
+
? {
|
|
58
|
+
message: "Invalid media service response",
|
|
59
|
+
code: "PROTOCOL",
|
|
60
|
+
retryable: false,
|
|
61
|
+
}
|
|
62
|
+
: {
|
|
63
|
+
message: failure.error,
|
|
64
|
+
code: failure.code,
|
|
65
|
+
stage: failure.stage,
|
|
66
|
+
retryable: failure.retryable,
|
|
67
|
+
};
|
|
68
|
+
if (failure?.provider !== undefined)
|
|
69
|
+
return context.json({
|
|
70
|
+
kind: "failed",
|
|
71
|
+
failure: { ...projected, provider: failure.provider },
|
|
72
|
+
});
|
|
73
|
+
return context.json({ kind: "failed", failure: projected });
|
|
71
74
|
}
|
|
72
75
|
if (response.status === 202) {
|
|
73
76
|
const pending = mediaPendingSchema.parse(await response.json());
|
|
@@ -186,7 +189,7 @@ export async function dispatchMediaBinding(env, frame, props, method, args, work
|
|
|
186
189
|
if (caller.computerId !== workspace.computerId ||
|
|
187
190
|
caller.actor.kind === "team-key")
|
|
188
191
|
throw new AppInstanceError("revoked");
|
|
189
|
-
const parsed = parseMediaBindingCall(
|
|
192
|
+
const parsed = parseMediaBindingCall(z.json().parse({
|
|
190
193
|
operation: method,
|
|
191
194
|
input: args[0],
|
|
192
195
|
}));
|
|
@@ -4,7 +4,7 @@ export declare const previewSelectorSchema: Schema.Struct<{
|
|
|
4
4
|
readonly ownerId: Schema.NonEmptyString;
|
|
5
5
|
readonly installationId: Schema.NonEmptyString;
|
|
6
6
|
readonly selection: Schema.Struct<{
|
|
7
|
-
readonly instanceId: Schema.Codec<import("../
|
|
7
|
+
readonly instanceId: Schema.Codec<import("../instance-id.js").InstanceId, string, never, never>;
|
|
8
8
|
readonly owner: Schema.Struct<{
|
|
9
9
|
readonly kind: Schema.Literals<readonly ["installation", "deployment"]>;
|
|
10
10
|
readonly id: Schema.NonEmptyString;
|
|
@@ -48,4 +48,4 @@ export declare function createStandalonePreview(storage: ConstructorParameters<t
|
|
|
48
48
|
fetch(signature: string, port: number, request: Request): Promise<Response>;
|
|
49
49
|
};
|
|
50
50
|
export declare function passivePreviewResponse(response: Response): Promise<Response>;
|
|
51
|
-
export declare function standalonePreviewRequest(request: Request, env: StandalonePreviewEnv, computerId: string
|
|
51
|
+
export declare function standalonePreviewRequest(request: Request, env: StandalonePreviewEnv, ownsComputer: (computerId: string) => Promise<boolean>, forward: (computerId: string, signature: string, port: number, request: Request) => Promise<Response>): Promise<Response | null>;
|
|
@@ -3,7 +3,7 @@ import * as z from "zod";
|
|
|
3
3
|
import { ComputerPortPreview } from "@fungi.computer/computer/preview";
|
|
4
4
|
import { selectionSchema } from "../index.js";
|
|
5
5
|
import { sha256DigestSchema } from "@fungi.computer/puffball/effect";
|
|
6
|
-
import { computerPortInputSchema } from "@fungi.computer/computer
|
|
6
|
+
import { computerPortInputSchema } from "@fungi.computer/computer-contract";
|
|
7
7
|
export const previewSelectorSchema = Schema.Struct({
|
|
8
8
|
ownerId: Schema.NonEmptyString,
|
|
9
9
|
installationId: Schema.NonEmptyString,
|
|
@@ -129,7 +129,7 @@ export async function passivePreviewResponse(response) {
|
|
|
129
129
|
});
|
|
130
130
|
return new Response(response.body, { status: response.status, headers });
|
|
131
131
|
}
|
|
132
|
-
export async function standalonePreviewRequest(request, env,
|
|
132
|
+
export async function standalonePreviewRequest(request, env, ownsComputer, forward) {
|
|
133
133
|
const incoming = new URL(request.url);
|
|
134
134
|
const previewHost = configuredPreviewOrigin(env);
|
|
135
135
|
// A host nothing configured as the preview tunnel is left to the caller's
|
|
@@ -143,23 +143,50 @@ export async function standalonePreviewRequest(request, env, computerId, forward
|
|
|
143
143
|
if (!["GET", "HEAD"].includes(request.method) ||
|
|
144
144
|
request.headers.has("Upgrade"))
|
|
145
145
|
return new Response(null, { status: 404 });
|
|
146
|
-
const path =
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
146
|
+
const path = new URLPattern({
|
|
147
|
+
pathname: "/preview/:computerId/:port/:signature/*",
|
|
148
|
+
}).exec(incoming)?.pathname.groups;
|
|
149
|
+
if (path === undefined)
|
|
150
150
|
return new Response(null, { status: 404 });
|
|
151
151
|
if (previewOrigins(env) === null)
|
|
152
152
|
return new Response(null, { status: 404 });
|
|
153
|
-
const
|
|
154
|
-
|
|
153
|
+
const ticket = z
|
|
154
|
+
.object({
|
|
155
|
+
computerId: z.string().min(1),
|
|
156
|
+
port: z.string().regex(/^[0-9]{1,5}$/u),
|
|
157
|
+
signature: z.string().regex(/^[a-z2-7]{20}$/u),
|
|
158
|
+
"0": z.string(),
|
|
159
|
+
})
|
|
160
|
+
.safeParse(path);
|
|
161
|
+
if (!ticket.success)
|
|
162
|
+
return new Response(null, { status: 404 });
|
|
163
|
+
let computerId;
|
|
164
|
+
try {
|
|
165
|
+
computerId = decodeURIComponent(ticket.data.computerId);
|
|
166
|
+
}
|
|
167
|
+
catch (cause) {
|
|
168
|
+
if (cause instanceof URIError)
|
|
169
|
+
return new Response(null, { status: 404 });
|
|
170
|
+
throw cause;
|
|
171
|
+
}
|
|
172
|
+
if (computerId.length === 0 ||
|
|
173
|
+
computerId.length > 256 ||
|
|
174
|
+
encodeURIComponent(computerId) !== ticket.data.computerId)
|
|
175
|
+
return new Response(null, { status: 404 });
|
|
176
|
+
const input = computerPortInputSchema.safeParse({
|
|
177
|
+
port: Number(ticket.data.port),
|
|
178
|
+
});
|
|
179
|
+
if (!input.success || String(input.data.port) !== ticket.data.port)
|
|
180
|
+
return new Response(null, { status: 404 });
|
|
181
|
+
if (!(await ownsComputer(computerId)))
|
|
155
182
|
return new Response(null, { status: 404 });
|
|
156
183
|
const service = new URL("http://localhost");
|
|
157
|
-
service.pathname =
|
|
184
|
+
service.pathname = `/${ticket.data["0"]}`;
|
|
158
185
|
service.search = incoming.search;
|
|
159
186
|
const headers = new Headers(request.headers);
|
|
160
187
|
for (const name of ["cookie", "authorization", "proxy-authorization", "host"])
|
|
161
188
|
headers.delete(name);
|
|
162
|
-
return forward(
|
|
189
|
+
return forward(computerId, ticket.data.signature, input.data.port, new Request(service, {
|
|
163
190
|
method: request.method,
|
|
164
191
|
headers,
|
|
165
192
|
signal: request.signal,
|