@fungi.computer/oyster 0.0.1 → 0.2.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.
- package/README.md +228 -176
- package/dist/agent-avatars.d.ts +103 -0
- package/dist/agent-avatars.js +66 -0
- package/dist/agents.d.ts +12 -2
- package/dist/agents.js +39 -2
- package/dist/app-builds.d.ts +76 -17
- package/dist/app-builds.js +207 -63
- package/dist/app-sources.d.ts +7 -22
- package/dist/app-sources.js +49 -14
- package/dist/apps/gate.d.ts +7 -7
- package/dist/apps/gate.js +6 -6
- package/dist/apps/installation.d.ts +35 -35
- package/dist/apps/installation.js +20 -19
- package/dist/build/artifact-writer.d.ts +40 -0
- package/dist/build/artifact-writer.js +181 -0
- package/dist/build/build-jobs.js +7 -1
- package/dist/build/build-policy.d.ts +10 -0
- package/dist/build/build-policy.js +10 -0
- package/dist/build/build-step.d.ts +12 -1
- package/dist/build/build-step.js +9 -1
- package/dist/build/candidate-builder.d.ts +68 -0
- package/dist/build/candidate-builder.js +91 -0
- package/dist/build/candidate-cli.d.ts +1 -0
- package/dist/build/candidate-cli.js +119 -0
- package/dist/build/candidate-cli.mjs +28220 -0
- package/dist/build/content-objects.d.ts +37 -0
- package/dist/build/content-objects.js +53 -0
- package/dist/build/effect.d.ts +18 -0
- package/dist/build/effect.js +52 -12
- package/dist/build/git-environment.d.ts +3 -0
- package/dist/build/git-environment.js +21 -9
- package/dist/build/local-build.d.ts +13 -3
- package/dist/build/local-build.js +149 -99
- package/dist/build/snapshot-package-store.d.ts +10 -0
- package/dist/build/snapshot-package-store.js +15 -0
- package/dist/build/snapshot-program-scripts.js +1 -1
- package/dist/build/snapshot-programs/snapshot-path.d.ts +2 -0
- package/dist/build/snapshot-programs/snapshot-path.js +6 -0
- package/dist/build/submission-contract.d.ts +461 -17
- package/dist/build/submission-contract.js +136 -26
- package/dist/build/workspace-build.d.ts +7 -2
- package/dist/build/workspace-build.js +55 -11
- package/dist/build-contract.d.ts +3 -0
- package/dist/build-contract.js +3 -0
- package/dist/build-egress.d.ts +62 -0
- package/dist/build-egress.js +148 -0
- package/dist/build.d.ts +1 -0
- package/dist/build.js +1 -0
- package/dist/cloudflare.js +4 -4
- package/dist/frame-binding-wire-script.js +1 -1
- package/dist/frame-binding-wire.d.ts +1 -1
- package/dist/frame-binding-wire.js +1 -1
- package/dist/git-journal.js +4 -4
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/installation-consent.d.ts +3 -1
- package/dist/installation-consent.js +2 -0
- package/dist/mcp-owner.js +20 -8
- package/dist/pack.d.ts +6 -3
- package/dist/pack.js +28 -11
- package/dist/page/gill-observation.d.ts +4 -0
- package/dist/page/gill-observation.js +31 -0
- package/dist/page/mount.d.ts +4 -8
- package/dist/page/window-launch.d.ts +18 -0
- package/dist/page/window-launch.js +11 -0
- package/dist/page-script.js +4 -4
- package/dist/release.d.ts +11 -0
- package/dist/release.js +4 -1
- package/dist/shell/app-directory.d.ts +50 -45
- package/dist/shell/app-directory.js +19 -18
- package/dist/shell/shell-module.d.ts +26 -2
- package/dist/shell/shell-module.js +51 -2
- package/dist/shell/window-manager.d.ts +5 -1
- package/dist/shell/window-manager.js +21 -2
- package/dist/shell/workbench-directory.d.ts +332 -0
- package/dist/shell/workbench-directory.js +35 -0
- package/dist/shell/workbench-host-types.d.ts +1 -1
- package/dist/shell/workbench-host.js +5 -0
- package/dist/shell/workbench-model.d.ts +3 -1
- package/dist/source/index.d.ts +6 -7
- package/dist/source/index.js +6 -7
- package/dist/source/template-push.d.ts +33 -0
- package/dist/source/template-push.js +223 -0
- package/dist/source/template-repository.d.ts +2 -0
- package/dist/source/template-repository.js +13 -0
- package/dist/source/{template-write.d.ts → template-seed.d.ts} +19 -20
- package/dist/source/template-seed.js +80 -0
- package/dist/standalone/agent-directory.d.ts +4 -0
- package/dist/standalone/agent-directory.js +4 -0
- package/dist/standalone/agents.d.ts +227 -0
- package/dist/standalone/agents.js +834 -0
- package/dist/standalone/app-builds-binding.d.ts +36 -0
- package/dist/standalone/app-builds-binding.js +119 -0
- package/dist/standalone/app-set.d.ts +91 -0
- package/dist/standalone/app-set.js +395 -0
- package/dist/standalone/app-source.d.ts +41 -0
- package/dist/standalone/app-source.js +59 -0
- package/dist/standalone/builds.d.ts +18 -0
- package/dist/standalone/builds.js +101 -0
- package/dist/standalone/computer-egress.d.ts +37 -0
- package/dist/standalone/computer-egress.js +111 -0
- package/dist/standalone/computer.d.ts +122 -0
- package/dist/standalone/computer.js +714 -0
- package/dist/standalone/install-apps-cli.d.ts +2 -0
- package/dist/standalone/install-apps-cli.js +126 -0
- package/dist/standalone/instance.d.ts +35 -0
- package/dist/standalone/instance.js +74 -0
- package/dist/standalone/media.d.ts +76 -0
- package/dist/standalone/media.js +227 -0
- package/dist/standalone/preview.d.ts +51 -0
- package/dist/standalone/preview.js +167 -0
- package/dist/standalone/registry.d.ts +102 -0
- package/dist/standalone/registry.js +193 -0
- package/dist/standalone/tenancy.d.ts +42 -0
- package/dist/standalone/tenancy.js +241 -0
- package/dist/standalone/worker.d.ts +55 -0
- package/dist/standalone/worker.js +147 -0
- package/dist/standalone/workflow-authority-readers.d.ts +16 -0
- package/dist/standalone/workflow-authority-readers.js +35 -0
- package/dist/standalone/workflow-binding.d.ts +216 -0
- package/dist/standalone/workflow-binding.js +217 -0
- package/dist/standalone/workflow-events.d.ts +46 -0
- package/dist/standalone/workflow-events.js +39 -0
- package/dist/standalone/workflow-git-capture.d.ts +7 -0
- package/dist/standalone/workflow-git-capture.js +62 -0
- package/dist/standalone/workflow-host.d.ts +955 -0
- package/dist/standalone/workflow-host.js +778 -0
- package/dist/standalone/workflow-node-capture.d.ts +13 -0
- package/dist/standalone/workflow-node-capture.js +72 -0
- package/dist/standalone/workflow-root-authority.d.ts +27 -0
- package/dist/standalone/workflow-root-authority.js +87 -0
- package/dist/standalone/workflow-schedules.d.ts +52 -0
- package/dist/standalone/workflow-schedules.js +40 -0
- package/dist/standalone/workflow-trigger-host.d.ts +1716 -0
- package/dist/standalone/workflow-trigger-host.js +52 -0
- package/dist/standalone/workflows.d.ts +713 -0
- package/dist/standalone/workflows.js +554 -0
- package/dist/tenancy/auth0.d.ts +64 -0
- package/dist/tenancy/auth0.js +198 -0
- package/dist/tenancy/cloudflare/rate-limit.d.ts +8 -0
- package/dist/tenancy/cloudflare/rate-limit.js +19 -0
- package/dist/tenancy/cloudflare/registry.d.ts +50 -0
- package/dist/tenancy/cloudflare/registry.js +301 -0
- package/dist/tenancy/cloudflare.d.ts +2 -0
- package/dist/tenancy/cloudflare.js +2 -0
- package/dist/tenancy/cold-directory.d.ts +13 -0
- package/dist/tenancy/cold-directory.js +43 -0
- package/dist/tenancy/contracts.d.ts +195 -0
- package/dist/tenancy/contracts.js +1 -0
- package/dist/tenancy/index.d.ts +19 -0
- package/dist/tenancy/index.js +156 -0
- package/dist/tenancy/owner-key.d.ts +26 -0
- package/dist/tenancy/owner-key.js +63 -0
- package/dist/tenancy/oyster.d.ts +4 -0
- package/dist/tenancy/oyster.js +25 -0
- package/dist/tenancy/registry-record.d.ts +7 -0
- package/dist/tenancy/registry-record.js +44 -0
- package/dist/tenancy/registry.d.ts +27 -0
- package/dist/tenancy/registry.js +138 -0
- package/dist/tenancy/resolve.d.ts +7 -0
- package/dist/tenancy/resolve.js +64 -0
- package/dist/workers-git.js +1 -4
- package/dist/workers.d.ts +2 -2
- package/dist/workers.js +3 -1
- package/docs/api/build-contract.md +829 -32
- package/docs/api/build.md +253 -17
- package/docs/api/modules.md +40 -32
- package/docs/api/pack.md +6 -3
- package/docs/api/shell/app-directory.md +267 -0
- package/docs/api/shell/shell-module.md +64 -3
- package/docs/api/shell/workbench-directory.md +119 -0
- package/docs/api/shell/workbench.md +19 -0
- package/docs/api/shell.md +127 -0
- package/docs/api/tenancy/auth0.md +110 -0
- package/docs/api/tenancy/cloudflare.md +221 -0
- package/docs/api/tenancy/cold-directory.md +46 -0
- package/docs/api/tenancy/owner-key.md +63 -0
- package/docs/api/tenancy/registry.md +77 -0
- package/docs/api/tenancy/resolve.md +69 -0
- package/docs/api/tenancy.md +430 -0
- package/docs/api/workers-git.md +140 -21
- package/docs/api/workers.md +15 -2
- package/docs/site/agent-quickstart.mdx +52 -55
- package/examples/standalone/worker.ts +7 -14
- package/examples/standalone/wrangler.jsonc +67 -6
- package/package.json +78 -25
- package/dist/build/r2-artifact-writer.d.ts +0 -31
- package/dist/build/r2-artifact-writer.js +0 -179
- package/dist/source/template-write.js +0 -344
- package/docs/media/architecture.mmd +0 -32
- package/docs/media/architecture.png +0 -0
- package/docs/media/bonsai-network.png +0 -0
- package/docs/media/bonsai-repository.png +0 -0
- package/examples/git/worker.ts +0 -39
- package/examples/git/wrangler.jsonc +0 -32
|
@@ -0,0 +1,554 @@
|
|
|
1
|
+
import { createCodemodeSandbox } from "@fungi.computer/mycelium/workers";
|
|
2
|
+
import { DurableObject } from "cloudflare:workers";
|
|
3
|
+
import * as S from "effect/Schema";
|
|
4
|
+
import { GraphWorkflow, } from "@fungi.computer/braid/cloudflare";
|
|
5
|
+
import {} from "@fungi.computer/braid";
|
|
6
|
+
import { standaloneTenancy } from "./tenancy.js";
|
|
7
|
+
import { createStandaloneWorkflowGitAccess, createStandaloneWorkflowRootAuthority, } from "./workflow-root-authority.js";
|
|
8
|
+
import { createStandaloneWorkflowHost } from "./workflow-host.js";
|
|
9
|
+
import { createStandaloneWorkflowSchedules, } from "./workflow-schedules.js";
|
|
10
|
+
import { createStandaloneWorkflowEvents, } from "./workflow-events.js";
|
|
11
|
+
import { createStandaloneWorkflowTriggerHost, standaloneTriggerErrors, } from "./workflow-trigger-host.js";
|
|
12
|
+
import { createWorkflowPublishedDefinitionPort, readPublishedWorkflow, } from "@fungi.computer/workflows-wire";
|
|
13
|
+
import { defaultTenant, tenantScope } from "../tenant.js";
|
|
14
|
+
import { AppInstanceError } from "../index.js";
|
|
15
|
+
import { workflowGitPassPrefix } from "../git.js";
|
|
16
|
+
import { createWorkflowGitPasses, createWorkflowGitPassStore, workflowGitPassWriter, workflowRunReport, wireRunInitiator, } from "@fungi.computer/braid/runtime";
|
|
17
|
+
import { canonicalJson } from "@fungi.computer/braid";
|
|
18
|
+
import { runKey, reviewKey, workflowGitPassRequestSchema, } from "./workflow-binding.js";
|
|
19
|
+
import { agentDirectoryForScope } from "./agent-directory.js";
|
|
20
|
+
/** SQLite and the native Workflow binding belong to this standalone host; its root supplies `hostFor`. */
|
|
21
|
+
export class StandaloneWorkflowRunsBase extends DurableObject {
|
|
22
|
+
composed;
|
|
23
|
+
/** Schedule time and its native alarm instant; production is the wall clock. */
|
|
24
|
+
scheduleClock = {
|
|
25
|
+
now: () => Date.now(),
|
|
26
|
+
alarmAt: (due) => due,
|
|
27
|
+
};
|
|
28
|
+
owner;
|
|
29
|
+
constructor(ctx, env) {
|
|
30
|
+
super(ctx, env);
|
|
31
|
+
// This object is named by its Workflows library installation; the alarm
|
|
32
|
+
// and App events have no library frame, so the first admitted trigger
|
|
33
|
+
// control records that name.
|
|
34
|
+
ctx.storage.sql
|
|
35
|
+
.exec(`CREATE TABLE IF NOT EXISTS standalone_workflow_library (
|
|
36
|
+
singleton INTEGER PRIMARY KEY CHECK (singleton = 1), installation_id TEXT NOT NULL
|
|
37
|
+
) STRICT`);
|
|
38
|
+
this.composed = ctx.blockConcurrencyWhile(async () => {
|
|
39
|
+
const shared = await standaloneTenancy(env);
|
|
40
|
+
const host = this.hostFor(tenantScope(defaultTenant, env.OYSTER_ORIGIN));
|
|
41
|
+
const agentToolPolicy = async (agentId) => (await host.appToolPolicy?.({ kind: "agent", agentId })) ?? null;
|
|
42
|
+
// A key's App access is its own grants (Ear Tag), read through
|
|
43
|
+
// `host.keys.live`, never through `appToolPolicy`.
|
|
44
|
+
const keyLive = async (keyId) => (await host.keys?.live(keyId)) ?? null;
|
|
45
|
+
// The one owner reaches every Agent; the report needs one that exists.
|
|
46
|
+
const admitDelivery = (_memberId, delivery) => agentDirectoryForScope(env, tenantScope(defaultTenant, env.OYSTER_ORIGIN)).has(delivery.agentId);
|
|
47
|
+
const owner = createStandaloneWorkflowHost(ctx.storage, host, {
|
|
48
|
+
authority(libraryInstallationId) {
|
|
49
|
+
return createStandaloneWorkflowRootAuthority({
|
|
50
|
+
shared,
|
|
51
|
+
host,
|
|
52
|
+
libraryInstallationId,
|
|
53
|
+
catalogHost: owner,
|
|
54
|
+
agentToolPolicy,
|
|
55
|
+
keyLive,
|
|
56
|
+
});
|
|
57
|
+
},
|
|
58
|
+
admitDelivery,
|
|
59
|
+
});
|
|
60
|
+
const triggersFor = (libraryId) => {
|
|
61
|
+
const authority = createStandaloneWorkflowRootAuthority({
|
|
62
|
+
shared,
|
|
63
|
+
host,
|
|
64
|
+
libraryInstallationId: libraryId,
|
|
65
|
+
catalogHost: owner,
|
|
66
|
+
agentToolPolicy,
|
|
67
|
+
keyLive,
|
|
68
|
+
});
|
|
69
|
+
const admitTrigger = async (memberId, installationId, computerId) => {
|
|
70
|
+
await authority.member(memberId);
|
|
71
|
+
if (installationId !== libraryId ||
|
|
72
|
+
!(await authority.memberComputer(memberId, computerId)))
|
|
73
|
+
throw standaloneTriggerErrors.denied();
|
|
74
|
+
return owner.triggerAuthority(installationId);
|
|
75
|
+
};
|
|
76
|
+
const triggers = createStandaloneWorkflowTriggerHost({
|
|
77
|
+
authority,
|
|
78
|
+
admitDelivery,
|
|
79
|
+
runs: {
|
|
80
|
+
sql: ctx.storage.sql,
|
|
81
|
+
transactionSync: (body) => ctx.storage.transactionSync(body),
|
|
82
|
+
teamId: () => libraryId,
|
|
83
|
+
},
|
|
84
|
+
triggers: {
|
|
85
|
+
published: createWorkflowPublishedDefinitionPort((git) => ({
|
|
86
|
+
published: (workflowId, at) => readPublishedWorkflow(git, workflowId, at),
|
|
87
|
+
})),
|
|
88
|
+
admit: async (memberId, installationId, computerId) => (await admitTrigger(memberId, installationId, computerId)).pin,
|
|
89
|
+
async source(memberId, installationId, computerId) {
|
|
90
|
+
const { installation } = await admitTrigger(memberId, installationId, computerId);
|
|
91
|
+
return (await host.git.git(installation)).binding({
|
|
92
|
+
kind: "member",
|
|
93
|
+
memberId,
|
|
94
|
+
});
|
|
95
|
+
},
|
|
96
|
+
// Standalone has no Connector-attachment store (Hub's own
|
|
97
|
+
// mechanism for Agent arming); an Agent's owner-approved tool
|
|
98
|
+
// policy governs dispatch, not arming here. See W-1/B-4(b).
|
|
99
|
+
readAgentAccess: () => Promise.resolve(null),
|
|
100
|
+
// The member's current Workflows Git policy; mints no pass itself.
|
|
101
|
+
admitGitAccess: async (memberId, repository, scope) => {
|
|
102
|
+
if (!(await gitAccess(libraryId)(memberId, repository, scope)))
|
|
103
|
+
throw standaloneTriggerErrors.denied();
|
|
104
|
+
},
|
|
105
|
+
launch: (run) => this.launch(libraryId, run),
|
|
106
|
+
},
|
|
107
|
+
recordCaller: (runId, memberId, computerId) => {
|
|
108
|
+
owner.recordTriggerCaller(runId, memberId, computerId);
|
|
109
|
+
},
|
|
110
|
+
});
|
|
111
|
+
const controls = {
|
|
112
|
+
sql: ctx.storage.sql,
|
|
113
|
+
triggers: triggers.triggers,
|
|
114
|
+
teamId: () => libraryId,
|
|
115
|
+
commit: (body) => Promise.resolve(ctx.storage.transactionSync(body)),
|
|
116
|
+
async authorize(memberId, installationId) {
|
|
117
|
+
await authority.member(memberId);
|
|
118
|
+
if (installationId !== libraryId)
|
|
119
|
+
throw standaloneTriggerErrors.denied();
|
|
120
|
+
},
|
|
121
|
+
admitInvocation: async (facts, operation) => {
|
|
122
|
+
const admitted = await owner.admitInvocation(facts, operation);
|
|
123
|
+
if (admitted.libraryId !== libraryId)
|
|
124
|
+
throw new AppInstanceError("revoked");
|
|
125
|
+
return admitted;
|
|
126
|
+
},
|
|
127
|
+
};
|
|
128
|
+
return {
|
|
129
|
+
schedules: createStandaloneWorkflowSchedules({
|
|
130
|
+
...controls,
|
|
131
|
+
now: () => this.scheduleClock.now(),
|
|
132
|
+
wake: (due) => due === null
|
|
133
|
+
? ctx.storage.deleteAlarm()
|
|
134
|
+
: ctx.storage.setAlarm(this.scheduleClock.alarmAt(due)),
|
|
135
|
+
reportFailure: ({ source, cause }) => {
|
|
136
|
+
console.error("Standalone Workflow schedule failed", source, cause);
|
|
137
|
+
},
|
|
138
|
+
}),
|
|
139
|
+
events: createStandaloneWorkflowEvents(controls),
|
|
140
|
+
};
|
|
141
|
+
};
|
|
142
|
+
let library = null;
|
|
143
|
+
const libraryTriggers = (libraryId) => {
|
|
144
|
+
this.nameLibrary(libraryId);
|
|
145
|
+
return (library ??= triggersFor(libraryId));
|
|
146
|
+
};
|
|
147
|
+
const computerId = host.computerId;
|
|
148
|
+
// Passes live with the run owner that records the Git steps they serve.
|
|
149
|
+
const passStore = createWorkflowGitPassStore({
|
|
150
|
+
sql: ctx.storage.sql,
|
|
151
|
+
transactionSync: (body) => ctx.storage.transactionSync(body),
|
|
152
|
+
ownerId: () => this.libraryId(),
|
|
153
|
+
computerId: () => computerId,
|
|
154
|
+
mintPassId: () => Array.from(crypto.getRandomValues(new Uint8Array(32)), (byte) => byte.toString(16).padStart(2, "0")).join(""),
|
|
155
|
+
remote: (_passId, repository) => {
|
|
156
|
+
const libraryId = this.libraryId();
|
|
157
|
+
if (repository.kind !== "installation" || libraryId === null)
|
|
158
|
+
throw new AppInstanceError("revoked");
|
|
159
|
+
// The tenant's ordinary smart-HTTP door; the pass never enters the
|
|
160
|
+
// URL and travels only as the bearer header.
|
|
161
|
+
return new URL(`/git/${repository.installationId}/${repository.repository}.git`, env.OYSTER_ORIGIN).href;
|
|
162
|
+
},
|
|
163
|
+
denied: () => new AppInstanceError("revoked"),
|
|
164
|
+
});
|
|
165
|
+
const gitAccess = (libraryId) => createStandaloneWorkflowGitAccess({
|
|
166
|
+
shared,
|
|
167
|
+
host,
|
|
168
|
+
libraryInstallationId: libraryId,
|
|
169
|
+
catalogHost: owner,
|
|
170
|
+
});
|
|
171
|
+
const gitPasses = (libraryId) => {
|
|
172
|
+
const access = gitAccess(libraryId);
|
|
173
|
+
return createWorkflowGitPasses({
|
|
174
|
+
denied: () => new AppInstanceError("revoked"),
|
|
175
|
+
admitNode: (context, target) => {
|
|
176
|
+
const node = owner.gitNode(context.idempotencyKey);
|
|
177
|
+
return node?.teamId === libraryId &&
|
|
178
|
+
node.installationId === target.installationId &&
|
|
179
|
+
node.method === target.method
|
|
180
|
+
? node
|
|
181
|
+
: null;
|
|
182
|
+
},
|
|
183
|
+
async admitMemberGitAccess(memberId, repository, scope) {
|
|
184
|
+
if (!(await access(memberId, repository, scope)))
|
|
185
|
+
throw new AppInstanceError("revoked");
|
|
186
|
+
},
|
|
187
|
+
open: (_computer, grant) => {
|
|
188
|
+
const pass = passStore.openWorkflowPass(grant);
|
|
189
|
+
return Promise.resolve({
|
|
190
|
+
...pass,
|
|
191
|
+
bearer: `${workflowGitPassPrefix}${libraryId}.${pass.passId}`,
|
|
192
|
+
});
|
|
193
|
+
},
|
|
194
|
+
revoke: (_computer, input) => {
|
|
195
|
+
passStore.revokeWorkflowRunGitPasses(input);
|
|
196
|
+
return Promise.resolve();
|
|
197
|
+
},
|
|
198
|
+
// Revocation runs in this object when the run ends; nothing is queued.
|
|
199
|
+
pending: () => [],
|
|
200
|
+
acknowledge: () => undefined,
|
|
201
|
+
});
|
|
202
|
+
};
|
|
203
|
+
return { owner, libraryTriggers, passStore, gitAccess, gitPasses };
|
|
204
|
+
});
|
|
205
|
+
this.owner = this.composed.then(({ owner }) => owner);
|
|
206
|
+
}
|
|
207
|
+
/** Record the library this object serves; another name is a revocation. */
|
|
208
|
+
nameLibrary(libraryId) {
|
|
209
|
+
const named = this.libraryId();
|
|
210
|
+
if (named !== null && named !== libraryId)
|
|
211
|
+
throw new AppInstanceError("revoked");
|
|
212
|
+
if (named === null)
|
|
213
|
+
this.ctx.storage.sql.exec("INSERT INTO standalone_workflow_library(singleton,installation_id) VALUES(1,?)", libraryId);
|
|
214
|
+
}
|
|
215
|
+
/**
|
|
216
|
+
* Mint (or renew) the Git pass for the Git step a node frame is executing.
|
|
217
|
+
* Null when this library dispatched no such step; a step that does not match
|
|
218
|
+
* the frame's attested Workflow, member and Computer is a revocation.
|
|
219
|
+
*/
|
|
220
|
+
async openWorkflowGitPass(raw) {
|
|
221
|
+
const request = workflowGitPassRequestSchema.parse(raw);
|
|
222
|
+
const { owner, gitPasses } = await this.composed;
|
|
223
|
+
const libraryId = this.libraryId();
|
|
224
|
+
const node = owner.gitNode(request.idempotencyKey);
|
|
225
|
+
if (libraryId === null || node === null)
|
|
226
|
+
return null;
|
|
227
|
+
if (node.teamId !== libraryId ||
|
|
228
|
+
node.installationId !== request.installationId ||
|
|
229
|
+
node.memberId !== request.memberId ||
|
|
230
|
+
node.computerId !== request.computerId ||
|
|
231
|
+
node.context.workflowId !== request.workflow.workflowId ||
|
|
232
|
+
node.context.runId !== request.workflow.runId ||
|
|
233
|
+
node.context.nodeId !== request.workflow.nodeId ||
|
|
234
|
+
canonicalJson(node.context.armedBy) !==
|
|
235
|
+
canonicalJson(request.workflow.armedBy))
|
|
236
|
+
throw new AppInstanceError("revoked");
|
|
237
|
+
return gitPasses(libraryId).openWorkflowPass(node.context, {
|
|
238
|
+
installationId: node.installationId,
|
|
239
|
+
method: node.method,
|
|
240
|
+
});
|
|
241
|
+
}
|
|
242
|
+
/** The tenant Git door's per-request admission of a presented pass. */
|
|
243
|
+
async admitWorkflowGitPass(passId, repository, scope) {
|
|
244
|
+
const { owner, passStore, gitAccess } = await this.composed;
|
|
245
|
+
const libraryId = this.libraryId();
|
|
246
|
+
const grant = passStore.admit({ passId, repository, scope });
|
|
247
|
+
if (libraryId === null ||
|
|
248
|
+
grant === null ||
|
|
249
|
+
owner.store.outcome(grant.runId) !== "active" ||
|
|
250
|
+
!(await gitAccess(libraryId)(grant.memberId, repository, scope)))
|
|
251
|
+
return null;
|
|
252
|
+
// The live root admission above holds only for the canonical owner.
|
|
253
|
+
return { ...workflowGitPassWriter(grant), role: "owner" };
|
|
254
|
+
}
|
|
255
|
+
/** A finished run's passes and recorded Git steps end with it. */
|
|
256
|
+
async endRunGit(runId) {
|
|
257
|
+
const { owner, passStore } = await this.composed;
|
|
258
|
+
const run = owner.store.read(runId);
|
|
259
|
+
if (run !== null)
|
|
260
|
+
passStore.revokeWorkflowRunGitPasses({ teamId: run.teamId, runId });
|
|
261
|
+
owner.forgetGitNodes(runId);
|
|
262
|
+
}
|
|
263
|
+
libraryId() {
|
|
264
|
+
const row = this.ctx.storage.sql
|
|
265
|
+
.exec("SELECT installation_id FROM standalone_workflow_library")
|
|
266
|
+
.toArray()[0];
|
|
267
|
+
return row === undefined
|
|
268
|
+
? null
|
|
269
|
+
: S.decodeUnknownSync(S.Struct({ installation_id: S.String }))(row)
|
|
270
|
+
.installation_id;
|
|
271
|
+
}
|
|
272
|
+
/** Trigger controls open the library named by the admitted frame. */
|
|
273
|
+
async library(facts, operation) {
|
|
274
|
+
const { owner, libraryTriggers } = await this.composed;
|
|
275
|
+
const { libraryId } = await owner.admitInvocation(facts, operation);
|
|
276
|
+
return libraryTriggers(libraryId);
|
|
277
|
+
}
|
|
278
|
+
/** Idempotently ensure a recorded run has its engine instance. */
|
|
279
|
+
async launch(installationId, run) {
|
|
280
|
+
try {
|
|
281
|
+
await this.env.GRAPH_WORKFLOWS.create({
|
|
282
|
+
id: run.runId,
|
|
283
|
+
params: {
|
|
284
|
+
kind: "run",
|
|
285
|
+
teamId: installationId,
|
|
286
|
+
runId: run.runId,
|
|
287
|
+
planDigest: run.planDigest,
|
|
288
|
+
},
|
|
289
|
+
});
|
|
290
|
+
}
|
|
291
|
+
catch (cause) {
|
|
292
|
+
// The durable run survives a lost create reply. Confirm its owned engine
|
|
293
|
+
// instance exists; unrelated create failures remain failures.
|
|
294
|
+
try {
|
|
295
|
+
await (await this.env.GRAPH_WORKFLOWS.get(run.runId)).status();
|
|
296
|
+
}
|
|
297
|
+
catch {
|
|
298
|
+
throw cause;
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
async armSchedule(facts, input) {
|
|
303
|
+
return (await this.library(facts, "armSchedule")).schedules.arm(facts, input);
|
|
304
|
+
}
|
|
305
|
+
async disarmSchedule(facts, input) {
|
|
306
|
+
return (await this.library(facts, "disarmSchedule")).schedules.disarm(facts, input);
|
|
307
|
+
}
|
|
308
|
+
async schedules(facts) {
|
|
309
|
+
return (await this.library(facts, "schedules")).schedules.list(facts);
|
|
310
|
+
}
|
|
311
|
+
async armEventTrigger(facts, input) {
|
|
312
|
+
return (await this.library(facts, "armEventTrigger")).events.arm(facts, input);
|
|
313
|
+
}
|
|
314
|
+
async disarmEventTrigger(facts, input) {
|
|
315
|
+
return (await this.library(facts, "disarmEventTrigger")).events.disarm(facts, input);
|
|
316
|
+
}
|
|
317
|
+
async eventTriggers(facts) {
|
|
318
|
+
return (await this.library(facts, "eventTriggers")).events.list(facts);
|
|
319
|
+
}
|
|
320
|
+
/** Up to 50 runs of this library, newest first; this host has no per-member rule. */
|
|
321
|
+
async runs(facts, input) {
|
|
322
|
+
const owner = await this.owner;
|
|
323
|
+
const { libraryId } = await owner.admitInvocation(facts, "runs");
|
|
324
|
+
const filter = input.workflowId === undefined
|
|
325
|
+
? { installationId: libraryId }
|
|
326
|
+
: { installationId: libraryId, workflowId: input.workflowId };
|
|
327
|
+
return {
|
|
328
|
+
runs: owner.store.list(filter).map((run) => {
|
|
329
|
+
const row = {
|
|
330
|
+
runId: run.runId,
|
|
331
|
+
workflowId: run.publication?.workflowId ?? run.trigger?.workflowId,
|
|
332
|
+
commit: run.publication?.commit ?? run.trigger?.commit,
|
|
333
|
+
source: run.source,
|
|
334
|
+
initiatedBy: wireRunInitiator(run.initiatedBy),
|
|
335
|
+
startedAt: run.startedAt,
|
|
336
|
+
};
|
|
337
|
+
return run.trigger === undefined
|
|
338
|
+
? row
|
|
339
|
+
: { ...row, triggerId: run.trigger.triggerId };
|
|
340
|
+
}),
|
|
341
|
+
};
|
|
342
|
+
}
|
|
343
|
+
/** Native alarm delivery: fire every due schedule of this library. */
|
|
344
|
+
async alarm() {
|
|
345
|
+
const { libraryTriggers } = await this.composed;
|
|
346
|
+
const libraryId = this.libraryId();
|
|
347
|
+
if (libraryId === null)
|
|
348
|
+
return;
|
|
349
|
+
await libraryTriggers(libraryId).schedules.alarm();
|
|
350
|
+
}
|
|
351
|
+
/**
|
|
352
|
+
* The emitting App's Instance delivers each accepted event here. The
|
|
353
|
+
* emitter's live Release must declare the event and its payload schema
|
|
354
|
+
* must accept it before any armed trigger of this library fires.
|
|
355
|
+
*/
|
|
356
|
+
async deliverEvent(fact) {
|
|
357
|
+
const { owner, libraryTriggers } = await this.composed;
|
|
358
|
+
const libraryId = this.libraryId();
|
|
359
|
+
if (libraryId === null)
|
|
360
|
+
return "accepted";
|
|
361
|
+
const admitted = await owner.admitEventFact(fact);
|
|
362
|
+
return libraryTriggers(libraryId).events.deliver(admitted);
|
|
363
|
+
}
|
|
364
|
+
async catalog(facts, input) {
|
|
365
|
+
const owner = await this.owner;
|
|
366
|
+
return owner.catalog(facts, input);
|
|
367
|
+
}
|
|
368
|
+
async start(facts, input) {
|
|
369
|
+
const owner = await this.owner;
|
|
370
|
+
const result = await owner.prepare(facts, input);
|
|
371
|
+
const installationId = facts.selection.owner.id;
|
|
372
|
+
if (!("kind" in result) ||
|
|
373
|
+
(result.kind !== "started" && result.kind !== "replay"))
|
|
374
|
+
return result;
|
|
375
|
+
if (owner.store.outcome(result.runId) !== "active")
|
|
376
|
+
return result;
|
|
377
|
+
this.nameLibrary(installationId);
|
|
378
|
+
await this.launch(installationId, result);
|
|
379
|
+
return result;
|
|
380
|
+
}
|
|
381
|
+
async loadRun(installationId, runId) {
|
|
382
|
+
const owner = await this.owner;
|
|
383
|
+
return owner.loadRun(installationId, runId);
|
|
384
|
+
}
|
|
385
|
+
async beforeNode(event) {
|
|
386
|
+
const owner = await this.owner;
|
|
387
|
+
return owner.beforeNode(event);
|
|
388
|
+
}
|
|
389
|
+
async dispatch(call) {
|
|
390
|
+
const owner = await this.owner;
|
|
391
|
+
return owner.dispatch(call);
|
|
392
|
+
}
|
|
393
|
+
async recordEvent(event) {
|
|
394
|
+
const owner = await this.owner;
|
|
395
|
+
owner.store.recordEvent(event);
|
|
396
|
+
}
|
|
397
|
+
async openReview(review) {
|
|
398
|
+
const owner = await this.owner;
|
|
399
|
+
return owner.store.openReview(review);
|
|
400
|
+
}
|
|
401
|
+
async answerReview(facts, raw) {
|
|
402
|
+
const owner = await this.owner;
|
|
403
|
+
const input = S.decodeUnknownSync(reviewKey)(raw, {
|
|
404
|
+
onExcessProperty: "error",
|
|
405
|
+
});
|
|
406
|
+
const { libraryId, context } = await (await this.owner).admitInvocation(facts, "answerReview");
|
|
407
|
+
const run = owner.store.read(input.runId);
|
|
408
|
+
if (run?.teamId !== libraryId || context.actor.kind !== "member")
|
|
409
|
+
return { result: "denied" };
|
|
410
|
+
const result = owner.store.answerReview(input, input.answer, {
|
|
411
|
+
kind: "member",
|
|
412
|
+
actorId: context.actor.memberId,
|
|
413
|
+
isOwner: context.role === "owner",
|
|
414
|
+
});
|
|
415
|
+
if (result === "denied" || result === "invalid")
|
|
416
|
+
return { result };
|
|
417
|
+
for (const delivery of owner.store.pendingReviewDeliveries(input.runId)) {
|
|
418
|
+
const instance = await this.env.GRAPH_WORKFLOWS.get(delivery.instanceId);
|
|
419
|
+
await owner.admitInvocation(facts, "answerReview");
|
|
420
|
+
if (owner.store.outcome(input.runId) !== "active")
|
|
421
|
+
break;
|
|
422
|
+
await instance.sendEvent({
|
|
423
|
+
type: delivery.eventType,
|
|
424
|
+
payload: delivery.answer,
|
|
425
|
+
});
|
|
426
|
+
owner.store.markReviewDelivered(delivery);
|
|
427
|
+
}
|
|
428
|
+
return { result };
|
|
429
|
+
}
|
|
430
|
+
/** Record the engine's ending, then report it to the run's declared delivery. */
|
|
431
|
+
async terminal(runId, ending) {
|
|
432
|
+
const owner = await this.owner;
|
|
433
|
+
owner.store.markTerminal(runId, "terminal");
|
|
434
|
+
await this.endRunGit(runId);
|
|
435
|
+
await this.report(runId, ending);
|
|
436
|
+
}
|
|
437
|
+
/**
|
|
438
|
+
* One idempotent follow-up into the run's declared delivery Session, however
|
|
439
|
+
* the run started, through the same Session send that the AGENTS binding
|
|
440
|
+
* uses. Delivery never changes the run's recorded outcome.
|
|
441
|
+
*/
|
|
442
|
+
async report(runId, ending) {
|
|
443
|
+
const owner = await this.owner;
|
|
444
|
+
const run = owner.store.read(runId);
|
|
445
|
+
const delivery = run?.delivery;
|
|
446
|
+
if (run === null || delivery === undefined)
|
|
447
|
+
return;
|
|
448
|
+
try {
|
|
449
|
+
await this.env.AGENTS.getByName(delivery.agentId).sendFromApp(run.teamId, delivery.sessionId, {
|
|
450
|
+
requestId: `workflow-run-report:${runId}`,
|
|
451
|
+
text: workflowRunReport(run.plan.definition.title, runId, ending),
|
|
452
|
+
delivery: "followUp",
|
|
453
|
+
});
|
|
454
|
+
}
|
|
455
|
+
catch (cause) {
|
|
456
|
+
console.error("Workflow run report was not delivered", runId, cause);
|
|
457
|
+
}
|
|
458
|
+
}
|
|
459
|
+
async status(facts, raw) {
|
|
460
|
+
const owner = await this.owner;
|
|
461
|
+
const { runId } = S.decodeUnknownSync(runKey)(raw, {
|
|
462
|
+
onExcessProperty: "error",
|
|
463
|
+
});
|
|
464
|
+
const { libraryId: installationId } = await (await this.owner).admitInvocation(facts, "graphStatus");
|
|
465
|
+
const run = owner.store.read(runId);
|
|
466
|
+
if (run?.teamId !== installationId)
|
|
467
|
+
return { error: "not_found" };
|
|
468
|
+
const outcome = owner.store.outcome(runId);
|
|
469
|
+
let status;
|
|
470
|
+
try {
|
|
471
|
+
status = await (await this.env.GRAPH_WORKFLOWS.get(runId)).status();
|
|
472
|
+
}
|
|
473
|
+
catch (cause) {
|
|
474
|
+
// The recorded run is visible before its engine instance is: while
|
|
475
|
+
// the run is still active, a failed instance lookup is that honest
|
|
476
|
+
// wait, not a fault. A settled run's lookup failure stays a failure.
|
|
477
|
+
if (outcome !== "active")
|
|
478
|
+
throw cause;
|
|
479
|
+
status = { status: "queued" };
|
|
480
|
+
}
|
|
481
|
+
return {
|
|
482
|
+
runId,
|
|
483
|
+
planDigest: run.plan.planDigest,
|
|
484
|
+
title: run.plan.definition.title,
|
|
485
|
+
outcome,
|
|
486
|
+
events: owner.store.events(runId),
|
|
487
|
+
reviews: owner.store.reviews(runId),
|
|
488
|
+
status: outcome === "cancelled" ? "terminated" : status.status,
|
|
489
|
+
output: outcome === "cancelled" ? null : (status.output ?? null),
|
|
490
|
+
error: status.error ?? null,
|
|
491
|
+
};
|
|
492
|
+
}
|
|
493
|
+
async cancel(facts, raw) {
|
|
494
|
+
const owner = await this.owner;
|
|
495
|
+
const { runId } = S.decodeUnknownSync(runKey)(raw, {
|
|
496
|
+
onExcessProperty: "error",
|
|
497
|
+
});
|
|
498
|
+
const { libraryId: installationId } = await (await this.owner).admitInvocation(facts, "cancel");
|
|
499
|
+
const run = owner.store.read(runId);
|
|
500
|
+
if (run?.teamId !== installationId)
|
|
501
|
+
return { kind: "refused", reason: "denied" };
|
|
502
|
+
if (owner.store.outcome(runId) !== "active")
|
|
503
|
+
return { kind: "terminal" };
|
|
504
|
+
const instance = await this.env.GRAPH_WORKFLOWS.get(runId);
|
|
505
|
+
try {
|
|
506
|
+
await instance.terminate();
|
|
507
|
+
}
|
|
508
|
+
catch (cause) {
|
|
509
|
+
const status = await instance.status();
|
|
510
|
+
if (status.status !== "terminated" &&
|
|
511
|
+
status.status !== "complete" &&
|
|
512
|
+
status.status !== "errored")
|
|
513
|
+
throw cause;
|
|
514
|
+
owner.store.markTerminal(runId, status.status === "terminated" ? "cancelled" : "terminal");
|
|
515
|
+
await this.endRunGit(runId);
|
|
516
|
+
if (status.status === "terminated")
|
|
517
|
+
await this.report(runId, { kind: "cancelled" });
|
|
518
|
+
return {
|
|
519
|
+
kind: status.status === "terminated" ? "cancelled" : "terminal",
|
|
520
|
+
};
|
|
521
|
+
}
|
|
522
|
+
owner.store.markTerminal(runId, "cancelled");
|
|
523
|
+
await this.endRunGit(runId);
|
|
524
|
+
await this.report(runId, { kind: "cancelled" });
|
|
525
|
+
return { kind: "cancelled" };
|
|
526
|
+
}
|
|
527
|
+
}
|
|
528
|
+
/** The SAME extracted interpreter as Hub, with this host's owned ports. */
|
|
529
|
+
export class StandaloneGraphWorkflow extends GraphWorkflow {
|
|
530
|
+
codeExecution() {
|
|
531
|
+
const localCpuUnenforced = "WORKFLOW_CODE_LOCAL_DEV" in this.env &&
|
|
532
|
+
this.env.WORKFLOW_CODE_LOCAL_DEV === "true";
|
|
533
|
+
return {
|
|
534
|
+
sandbox: createCodemodeSandbox(this.env.LOADER, "workflow-code", {
|
|
535
|
+
localDevUnenforcedLimits: localCpuUnenforced ? ["cpu"] : [],
|
|
536
|
+
}),
|
|
537
|
+
// Workflow data transforms have a host-owned 30-second execution budget.
|
|
538
|
+
timeoutMs: 30_000,
|
|
539
|
+
};
|
|
540
|
+
}
|
|
541
|
+
dispatcher(params) {
|
|
542
|
+
const runs = this.env.WORKFLOW_RUNS.getByName(params.teamId);
|
|
543
|
+
return {
|
|
544
|
+
loadRun: (installationId, runId) => runs.loadRun(installationId, runId),
|
|
545
|
+
beforeNode: (event) => runs.beforeNode(event),
|
|
546
|
+
dispatch: (call) => runs.dispatch(call),
|
|
547
|
+
recordEvent: (event) => runs.recordEvent(event),
|
|
548
|
+
openReview: (review) => runs.openReview(review),
|
|
549
|
+
};
|
|
550
|
+
}
|
|
551
|
+
terminalTriggerRun(params, ending) {
|
|
552
|
+
return this.env.WORKFLOW_RUNS.getByName(params.teamId).terminal(params.runId, ending);
|
|
553
|
+
}
|
|
554
|
+
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import type { TenantId } from "../tenant.js";
|
|
2
|
+
import type { AuthAdapter, Identity, ReadonlyTenantRegistry, ReadonlyTenancy } from "./contracts.js";
|
|
3
|
+
/** One short-lived, single-use browser authorization transaction. */
|
|
4
|
+
export type Auth0Transaction = Readonly<{
|
|
5
|
+
/** Tenant selected at login by the host resolver. */
|
|
6
|
+
tenantId: TenantId;
|
|
7
|
+
/** Exact canonical callback URI supplied to the provider. */
|
|
8
|
+
redirectUri: string;
|
|
9
|
+
/** State bound to the browser transaction cookie. */
|
|
10
|
+
state: string;
|
|
11
|
+
/** Secret verifier held server-side until the one-use exchange. */
|
|
12
|
+
codeVerifier: string;
|
|
13
|
+
/** Absolute expiration time in milliseconds. */
|
|
14
|
+
expiresAt: number;
|
|
15
|
+
}>;
|
|
16
|
+
/** Server-owned verified identity session; browsers receive only its random handle. */
|
|
17
|
+
export type Auth0Session = Readonly<{
|
|
18
|
+
/** Tenant for which this identity was admitted at callback time. */
|
|
19
|
+
tenantId: TenantId;
|
|
20
|
+
/** Verified issuer/subject; live registry membership still applies per request. */
|
|
21
|
+
identity: Identity;
|
|
22
|
+
/** Absolute expiration, no later than the verified access token's expiry. */
|
|
23
|
+
expiresAt: number;
|
|
24
|
+
}>;
|
|
25
|
+
/** Host ports for optional browser login; stored state must be shared across host instances. */
|
|
26
|
+
export type Auth0BrowserOptions = Readonly<{
|
|
27
|
+
/** Full callback pathname, including the prefix used to mount routes. */
|
|
28
|
+
callbackPath: string;
|
|
29
|
+
/** Use the kit's resolver; never accept a tenant from callback query/body data. */
|
|
30
|
+
resolveTenant: ReadonlyTenancy["resolve"];
|
|
31
|
+
/** Current tenant and membership authority, checked before issuing a session. */
|
|
32
|
+
registry: Pick<ReadonlyTenantRegistry, "get" | "membershipOf">;
|
|
33
|
+
/** Host-owned transaction storage with atomic one-use consumption. */
|
|
34
|
+
transactions: Readonly<{
|
|
35
|
+
/** Store until expiresAt; reject duplicate handles. */
|
|
36
|
+
put(id: string, transaction: Auth0Transaction): Promise<void>;
|
|
37
|
+
/** Atomically remove and return the transaction, or null if missing/consumed. */
|
|
38
|
+
take(id: string): Promise<Auth0Transaction | null>;
|
|
39
|
+
}>;
|
|
40
|
+
/** Host-owned session storage; expire and prune records at expiresAt. */
|
|
41
|
+
sessions: Readonly<{
|
|
42
|
+
/** Store a fresh opaque handle and verified identity until expiry. */
|
|
43
|
+
put(id: string, session: Auth0Session): Promise<void>;
|
|
44
|
+
/** Read a session by its opaque handle, or null after expiry/removal. */
|
|
45
|
+
get(id: string): Promise<Auth0Session | null>;
|
|
46
|
+
}>;
|
|
47
|
+
}>;
|
|
48
|
+
/** Auth0 issuer and audience configuration; browser login is opt-in. */
|
|
49
|
+
export type Auth0Options = Readonly<{
|
|
50
|
+
/** Auth0 issuer hostname, including a configured custom hostname. */
|
|
51
|
+
domain: string;
|
|
52
|
+
/** Registered application's client identifier. */
|
|
53
|
+
clientId: string;
|
|
54
|
+
/** Identifier of the API for which access tokens are issued. */
|
|
55
|
+
audience: string;
|
|
56
|
+
/** Optional confidential-client secret; omit for a registered public PKCE client. */
|
|
57
|
+
clientSecret?: string;
|
|
58
|
+
/** Optional host-owned transport for issuer discovery, keys and token exchange. */
|
|
59
|
+
fetch?: typeof globalThis.fetch;
|
|
60
|
+
/** Browser routes and durable host-owned transaction/session storage. */
|
|
61
|
+
browser?: Auth0BrowserOptions;
|
|
62
|
+
}>;
|
|
63
|
+
/** Verify Auth0 identities; the registry alone supplies canonical membership and role. */
|
|
64
|
+
export declare function auth0(options: Auth0Options): AuthAdapter;
|