@fungi.computer/oyster 0.0.0-stage → 0.1.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/LICENSE +21 -0
- package/README.md +1134 -2
- package/dist/agents.d.ts +114 -0
- package/dist/agents.js +322 -0
- package/dist/app-builds.d.ts +210 -0
- package/dist/app-builds.js +499 -0
- package/dist/app-sandbox.d.ts +2 -0
- package/dist/app-sandbox.js +2 -0
- package/dist/app-sources.d.ts +31 -0
- package/dist/app-sources.js +370 -0
- package/dist/app-storage.d.ts +141 -0
- package/dist/app-storage.js +281 -0
- package/dist/apps/gate.d.ts +105 -0
- package/dist/apps/gate.js +273 -0
- package/dist/apps/index.d.ts +5 -0
- package/dist/apps/index.js +5 -0
- package/dist/apps/installation.d.ts +248 -0
- package/dist/apps/installation.js +307 -0
- package/dist/apps/operations.d.ts +8 -0
- package/dist/apps/operations.js +98 -0
- package/dist/apps/tools.d.ts +33 -0
- package/dist/apps/tools.js +17 -0
- package/dist/auth.d.ts +11 -0
- package/dist/auth.js +93 -0
- package/dist/backend-request.d.ts +3 -0
- package/dist/backend-request.js +53 -0
- package/dist/build/artifact-writer.d.ts +40 -0
- package/dist/build/artifact-writer.js +181 -0
- package/dist/build/build-jobs.d.ts +128 -0
- package/dist/build/build-jobs.js +203 -0
- package/dist/build/build-policy.d.ts +10 -0
- package/dist/build/build-policy.js +10 -0
- package/dist/build/build-step.d.ts +37 -0
- package/dist/build/build-step.js +28 -0
- package/dist/build/bundled-html.d.ts +7 -0
- package/dist/build/bundled-html.js +64 -0
- 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 +28170 -0
- package/dist/build/content-objects.d.ts +37 -0
- package/dist/build/content-objects.js +53 -0
- package/dist/build/definition-emission.d.ts +34 -0
- package/dist/build/definition-emission.js +201 -0
- package/dist/build/effect.d.ts +249 -0
- package/dist/build/effect.js +436 -0
- package/dist/build/git-environment.d.ts +9 -0
- package/dist/build/git-environment.js +60 -0
- package/dist/build/html-recipe.d.ts +20 -0
- package/dist/build/html-recipe.js +51 -0
- package/dist/build/local-build.d.ts +67 -0
- package/dist/build/local-build.js +1128 -0
- package/dist/build/snapshot-package-store.d.ts +12 -0
- package/dist/build/snapshot-package-store.js +36 -0
- package/dist/build/snapshot-program-scripts.d.ts +6 -0
- package/dist/build/snapshot-program-scripts.js +2 -0
- 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 +828 -0
- package/dist/build/submission-contract.js +180 -0
- package/dist/build/workspace-build.d.ts +55 -0
- package/dist/build/workspace-build.js +200 -0
- package/dist/build-contract.d.ts +7 -0
- package/dist/build-contract.js +7 -0
- package/dist/build-egress.d.ts +62 -0
- package/dist/build-egress.js +148 -0
- package/dist/build-jobs.d.ts +2 -0
- package/dist/build-jobs.js +2 -0
- package/dist/build.d.ts +12 -0
- package/dist/build.js +14 -0
- package/dist/capability-descriptor.d.ts +46 -0
- package/dist/capability-descriptor.js +51 -0
- package/dist/catalog-frame.d.ts +39 -0
- package/dist/catalog-frame.js +72 -0
- package/dist/cause-class.d.ts +19 -0
- package/dist/cause-class.js +54 -0
- package/dist/cloudflare.d.ts +123 -0
- package/dist/cloudflare.js +391 -0
- package/dist/computer-terminal.d.ts +38 -0
- package/dist/computer-terminal.js +410 -0
- package/dist/computer.d.ts +60 -0
- package/dist/computer.js +33 -0
- package/dist/event-recipients.d.ts +57 -0
- package/dist/event-recipients.js +144 -0
- package/dist/facet-invocation.d.ts +4 -0
- package/dist/facet-invocation.js +45 -0
- package/dist/files.d.ts +42 -0
- package/dist/files.js +123 -0
- package/dist/frame-binding-wire-script.d.ts +2 -0
- package/dist/frame-binding-wire-script.js +1 -0
- package/dist/frame-binding-wire.d.ts +28 -0
- package/dist/frame-binding-wire.js +54 -0
- package/dist/git-admission.d.ts +52 -0
- package/dist/git-admission.js +188 -0
- package/dist/git-auth.d.ts +11 -0
- package/dist/git-auth.js +27 -0
- package/dist/git-door.d.ts +35 -0
- package/dist/git-door.js +108 -0
- package/dist/git-journal.d.ts +31 -0
- package/dist/git-journal.js +188 -0
- package/dist/git.d.ts +31 -0
- package/dist/git.js +64 -0
- package/dist/guest-policy.d.ts +9 -0
- package/dist/guest-policy.js +13 -0
- package/dist/host.d.ts +244 -0
- package/dist/host.js +1043 -0
- package/dist/index.d.ts +166 -0
- package/dist/index.js +320 -0
- package/dist/installation-consent.d.ts +79 -0
- package/dist/installation-consent.js +82 -0
- package/dist/internal/background-origins.d.ts +3 -0
- package/dist/internal/background-origins.js +8 -0
- package/dist/internal/delivery.d.ts +12 -0
- package/dist/internal/delivery.js +167 -0
- package/dist/internal/host-gate.d.ts +14 -0
- package/dist/internal/host-gate.js +62 -0
- package/dist/internal/registry-installations.d.ts +6 -0
- package/dist/internal/registry-installations.js +12 -0
- package/dist/invocation.d.ts +251 -0
- package/dist/invocation.js +377 -0
- package/dist/keys.d.ts +48 -0
- package/dist/keys.js +52 -0
- package/dist/mcp-owner.d.ts +27 -0
- package/dist/mcp-owner.js +155 -0
- package/dist/mcp.d.ts +89 -0
- package/dist/mcp.js +429 -0
- package/dist/media.d.ts +23 -0
- package/dist/media.js +12 -0
- package/dist/pack.d.ts +79 -0
- package/dist/pack.js +151 -0
- package/dist/page/capabilities.d.ts +23 -0
- package/dist/page/capabilities.js +256 -0
- package/dist/page/computer-handles.d.ts +79 -0
- package/dist/page/computer-handles.js +44 -0
- package/dist/page/gill-observation.d.ts +4 -0
- package/dist/page/gill-observation.js +31 -0
- package/dist/page/lifetime.d.ts +6 -0
- package/dist/page/lifetime.js +19 -0
- package/dist/page/mount.d.ts +163 -0
- package/dist/page/mount.js +390 -0
- package/dist/page/storage.d.ts +3 -0
- package/dist/page/storage.js +82 -0
- package/dist/page/terminal.d.ts +5 -0
- package/dist/page/terminal.js +7 -0
- package/dist/page/window-launch.d.ts +18 -0
- package/dist/page/window-launch.js +11 -0
- package/dist/page-script.d.ts +14 -0
- package/dist/page-script.js +5 -0
- package/dist/r2-files.d.ts +3 -0
- package/dist/r2-files.js +79 -0
- package/dist/release.d.ts +83 -0
- package/dist/release.js +167 -0
- package/dist/root-data.d.ts +86 -0
- package/dist/root-data.js +69 -0
- package/dist/shell/app-directory.d.ts +542 -0
- package/dist/shell/app-directory.js +199 -0
- package/dist/shell/browser-actions.d.ts +7 -0
- package/dist/shell/browser-actions.js +56 -0
- package/dist/shell/command-host.d.ts +112 -0
- package/dist/shell/command-host.js +502 -0
- package/dist/shell/commands.d.ts +25 -0
- package/dist/shell/commands.js +78 -0
- package/dist/shell/index.d.ts +8 -0
- package/dist/shell/index.js +8 -0
- package/dist/shell/keys.d.ts +41 -0
- package/dist/shell/keys.js +134 -0
- package/dist/shell/shell-module.d.ts +55 -0
- package/dist/shell/shell-module.js +86 -0
- package/dist/shell/window-manager.d.ts +124 -0
- package/dist/shell/window-manager.js +711 -0
- package/dist/shell/workbench-contract.d.ts +68 -0
- package/dist/shell/workbench-contract.js +1 -0
- 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 +96 -0
- package/dist/shell/workbench-host-types.js +1 -0
- package/dist/shell/workbench-host.d.ts +34 -0
- package/dist/shell/workbench-host.js +347 -0
- package/dist/shell/workbench-model.d.ts +192 -0
- package/dist/shell/workbench-model.js +53 -0
- package/dist/shell/workbench.d.ts +4 -0
- package/dist/shell/workbench.js +2 -0
- package/dist/source/fork.d.ts +27 -0
- package/dist/source/fork.js +46 -0
- package/dist/source/index.d.ts +10 -0
- package/dist/source/index.js +10 -0
- package/dist/source/seed-receipt.d.ts +53 -0
- package/dist/source/seed-receipt.js +18 -0
- package/dist/source/team-source.d.ts +32 -0
- package/dist/source/team-source.js +42 -0
- package/dist/source/template-push.d.ts +33 -0
- package/dist/source/template-push.js +223 -0
- package/dist/source/template-repository.d.ts +4 -0
- package/dist/source/template-repository.js +21 -0
- package/dist/source/template-seed.d.ts +68 -0
- 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 +220 -0
- package/dist/standalone/agents.js +795 -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 +931 -0
- package/dist/standalone/workflow-host.js +751 -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 +1694 -0
- package/dist/standalone/workflow-trigger-host.js +52 -0
- package/dist/standalone/workflows.d.ts +657 -0
- package/dist/standalone/workflows.js +554 -0
- package/dist/storage-frame.d.ts +64 -0
- package/dist/storage-frame.js +28 -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/tenant.d.ts +81 -0
- package/dist/tenant.js +71 -0
- package/dist/testing/capabilities.d.ts +29 -0
- package/dist/testing/capabilities.js +130 -0
- package/dist/testing/catalog.d.ts +3 -0
- package/dist/testing/catalog.js +53 -0
- package/dist/testing/index.d.ts +10 -0
- package/dist/testing/index.js +10 -0
- package/dist/testing/installation.d.ts +11 -0
- package/dist/testing/installation.js +25 -0
- package/dist/testing/native-worker.d.ts +21 -0
- package/dist/testing/native-worker.js +106 -0
- package/dist/testing/worker.d.ts +18 -0
- package/dist/testing/worker.js +67 -0
- package/dist/testing/workers-runtime.d.ts +24 -0
- package/dist/testing/workers-runtime.js +33 -0
- package/dist/webhook-delivery.d.ts +14 -0
- package/dist/webhook-delivery.js +17 -0
- package/dist/workers-git.d.ts +285 -0
- package/dist/workers-git.js +1174 -0
- package/dist/workers.d.ts +352 -0
- package/dist/workers.js +868 -0
- package/docs/api/README.md +82 -0
- package/docs/api/agents.md +332 -0
- package/docs/api/app-storage.md +209 -0
- package/docs/api/apps.md +1375 -0
- package/docs/api/auth.md +63 -0
- package/docs/api/build-contract.md +1304 -0
- package/docs/api/build.md +1712 -0
- package/docs/api/cloudflare.md +421 -0
- package/docs/api/computer.md +203 -0
- package/docs/api/files.md +117 -0
- package/docs/api/git.md +470 -0
- package/docs/api/host.md +1328 -0
- package/docs/api/index.md +430 -0
- package/docs/api/invocation.md +738 -0
- package/docs/api/keys.md +175 -0
- package/docs/api/mcp.md +301 -0
- package/docs/api/media.md +40 -0
- package/docs/api/modules.md +50 -0
- package/docs/api/pack.md +141 -0
- package/docs/api/page/mount.md +298 -0
- package/docs/api/root-data.md +111 -0
- package/docs/api/shell/app-directory.md +599 -0
- package/docs/api/shell/command-host.md +224 -0
- package/docs/api/shell/commands.md +197 -0
- package/docs/api/shell/keys.md +183 -0
- package/docs/api/shell/shell-module.md +204 -0
- package/docs/api/shell/window-manager.md +267 -0
- package/docs/api/shell/workbench-directory.md +119 -0
- package/docs/api/shell/workbench.md +205 -0
- package/docs/api/shell.md +1915 -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/tenant.md +194 -0
- package/docs/api/workers-git.md +1646 -0
- package/docs/api/workers.md +1691 -0
- package/docs/site/agent-quickstart.mdx +346 -0
- package/examples/standalone/worker.ts +7 -0
- package/examples/standalone/wrangler.jsonc +82 -0
- package/package.json +300 -3
|
@@ -0,0 +1,751 @@
|
|
|
1
|
+
import * as S from "effect/Schema";
|
|
2
|
+
import { admitRunInput, accepts, catalogEvents, catalogMethods, catalogSchema, contentDigest, canonicalJson, parseProjection, nodeEventSchema, } from "@fungi.computer/braid";
|
|
3
|
+
import { deriveDeclaredOperations, approvedBindingGrants, grantsFor, } from "../apps/index.js";
|
|
4
|
+
import { pendingSchema } from "@fungi.computer/agar/backend-definition";
|
|
5
|
+
import { verifyRelease, } from "../host.js";
|
|
6
|
+
import { APP_INVOCATION_CEILING_MS, invocationAuthorityOf, invocationFacts, invocationFactsSchema, } from "../invocation.js";
|
|
7
|
+
import { AppInstanceError } from "../index.js";
|
|
8
|
+
import { appCapabilityIds } from "@fungi.computer/puffball";
|
|
9
|
+
import { createGraphWorkflowRuns, compileForMember, graphStartRequestSchema, preparationFromRun, planNodeCall, workflowRunContextSchema, } from "@fungi.computer/braid/runtime";
|
|
10
|
+
import { appMethodCallContextSchema } from "@fungi.computer/puffball/app";
|
|
11
|
+
import { appEventFactSchema, } from "@fungi.computer/puffball/app-event-facts";
|
|
12
|
+
import { keyAppAccess } from "../keys.js";
|
|
13
|
+
import { Validator } from "@cfworker/json-schema";
|
|
14
|
+
// Mirrors packages/oyster/src/cloudflare.ts's MAX_APP_REPLY_BYTES. That
|
|
15
|
+
// module imports "cloudflare:workers" at the top level, so it cannot be
|
|
16
|
+
// imported for its value outside a Workers runtime (e.g. this file's own
|
|
17
|
+
// node/vitest tests); apps/hub/src/app-workflow-broker.ts duplicates the
|
|
18
|
+
// same literal for the same reason.
|
|
19
|
+
const MAX_APP_REPLY_BYTES = 240 * 1024;
|
|
20
|
+
/**
|
|
21
|
+
* The key's own catalog view: an installation it cannot reach at all drops
|
|
22
|
+
* out, and a read-only installation keeps only its read methods. A node
|
|
23
|
+
* that reaches past the key then fails to compile with the engine's own
|
|
24
|
+
* "Installation is not available to you" or unknown-method issue -- never a
|
|
25
|
+
* live grant computed here.
|
|
26
|
+
*/
|
|
27
|
+
function keyCatalogEntries(entries, key) {
|
|
28
|
+
const kept = [];
|
|
29
|
+
for (const entry of entries) {
|
|
30
|
+
const access = keyAppAccess(key, entry.installationId);
|
|
31
|
+
if (access === null)
|
|
32
|
+
continue;
|
|
33
|
+
if (access.level === "write") {
|
|
34
|
+
kept.push(entry);
|
|
35
|
+
continue;
|
|
36
|
+
}
|
|
37
|
+
const methods = Object.fromEntries(Object.entries(entry.methods).filter(([, method]) => method.effect === "read"));
|
|
38
|
+
kept.push({ ...entry, methods });
|
|
39
|
+
}
|
|
40
|
+
return kept;
|
|
41
|
+
}
|
|
42
|
+
const catalogProjectionSchema = S.Struct({
|
|
43
|
+
...catalogSchema.value.fields,
|
|
44
|
+
displayName: S.String,
|
|
45
|
+
icon: S.optionalKey(S.String),
|
|
46
|
+
capabilities: S.optionalKey(S.Array(S.Literals(appCapabilityIds))),
|
|
47
|
+
});
|
|
48
|
+
const catalogRecordSchema = S.Struct({
|
|
49
|
+
release_key: S.String,
|
|
50
|
+
record: S.String,
|
|
51
|
+
});
|
|
52
|
+
const catalogProjectionCodec = S.fromJsonString(S.NullOr(catalogProjectionSchema));
|
|
53
|
+
/** Cached rows from another projection shape are recomputed, never read. */
|
|
54
|
+
const catalogProjectionVersion = "methods+events";
|
|
55
|
+
const releaseKey = (installation) => JSON.stringify([
|
|
56
|
+
catalogProjectionVersion,
|
|
57
|
+
installation.release.appId,
|
|
58
|
+
installation.release.releaseId,
|
|
59
|
+
installation.release.artifact.artifactRef,
|
|
60
|
+
installation.release.artifact.artifactDigest,
|
|
61
|
+
]);
|
|
62
|
+
function summarizedMethod(method) {
|
|
63
|
+
const summary = {
|
|
64
|
+
description: method.description,
|
|
65
|
+
effect: method.effect,
|
|
66
|
+
approval: method.approval,
|
|
67
|
+
requires: method.requires,
|
|
68
|
+
digest: method.digest,
|
|
69
|
+
};
|
|
70
|
+
if (method.pending === true)
|
|
71
|
+
return { ...summary, pending: true };
|
|
72
|
+
return summary;
|
|
73
|
+
}
|
|
74
|
+
const summarizedEntry = (entry) => {
|
|
75
|
+
const summary = {
|
|
76
|
+
installationId: entry.installationId,
|
|
77
|
+
appId: entry.appId,
|
|
78
|
+
displayName: entry.displayName,
|
|
79
|
+
workflowPublishGranted: entry.workflowPublishGranted ?? false,
|
|
80
|
+
workflowRunGranted: entry.workflowRunGranted ?? false,
|
|
81
|
+
methods: Object.fromEntries(Object.entries(entry.methods).map(([name, method]) => [
|
|
82
|
+
name,
|
|
83
|
+
summarizedMethod(method),
|
|
84
|
+
])),
|
|
85
|
+
};
|
|
86
|
+
if (entry.icon !== undefined)
|
|
87
|
+
summary.icon = entry.icon;
|
|
88
|
+
if (entry.capabilities !== undefined)
|
|
89
|
+
summary.capabilities = entry.capabilities;
|
|
90
|
+
if (entry.events !== undefined)
|
|
91
|
+
summary.events = Object.fromEntries(Object.entries(entry.events).map(([name, event]) => [
|
|
92
|
+
name,
|
|
93
|
+
{ description: event.description, digest: event.digest },
|
|
94
|
+
]));
|
|
95
|
+
return summary;
|
|
96
|
+
};
|
|
97
|
+
/** The standalone host's installation admission, above the portable engine. */
|
|
98
|
+
export function createStandaloneWorkflowHost(owner, host, options) {
|
|
99
|
+
const store = createGraphWorkflowRuns(owner);
|
|
100
|
+
// Only the host's attested invocation context is associated here. Lifecycle,
|
|
101
|
+
// events, reviews and replay are owned by the shared runtime store.
|
|
102
|
+
owner.sql.exec(`CREATE TABLE IF NOT EXISTS standalone_workflow_callers (
|
|
103
|
+
run_id TEXT PRIMARY KEY, caller TEXT NOT NULL
|
|
104
|
+
) STRICT`);
|
|
105
|
+
const readCaller = (runId) => {
|
|
106
|
+
const row = owner.sql
|
|
107
|
+
.exec("SELECT caller FROM standalone_workflow_callers WHERE run_id=?", runId)
|
|
108
|
+
.toArray()[0];
|
|
109
|
+
if (row === undefined)
|
|
110
|
+
return null;
|
|
111
|
+
const saved = S.decodeUnknownSync(S.Struct({ caller: S.String }))(row);
|
|
112
|
+
return S.decodeUnknownSync(appMethodCallContextSchema)(JSON.parse(saved.caller));
|
|
113
|
+
};
|
|
114
|
+
const caller = (runId) => {
|
|
115
|
+
const saved = readCaller(runId);
|
|
116
|
+
if (saved === null)
|
|
117
|
+
throw new Error("Workflow caller is unavailable");
|
|
118
|
+
return saved;
|
|
119
|
+
};
|
|
120
|
+
// A dispatched Git step, keyed by its engine call identity: the only
|
|
121
|
+
// admission a Workflow Git pass mint accepts. Removed when the run ends.
|
|
122
|
+
owner.sql.exec(`CREATE TABLE IF NOT EXISTS standalone_workflow_git_nodes (
|
|
123
|
+
idempotency_key TEXT PRIMARY KEY, run_id TEXT NOT NULL, record TEXT NOT NULL
|
|
124
|
+
) STRICT`);
|
|
125
|
+
const gitNodeCodec = S.fromJsonString(S.Struct({
|
|
126
|
+
teamId: S.String,
|
|
127
|
+
memberId: S.String,
|
|
128
|
+
installationId: S.String,
|
|
129
|
+
appId: S.String,
|
|
130
|
+
method: S.String,
|
|
131
|
+
methodDigest: S.String,
|
|
132
|
+
computerId: S.NullOr(S.String),
|
|
133
|
+
context: workflowRunContextSchema,
|
|
134
|
+
}));
|
|
135
|
+
const recordGitNode = (idempotencyKey, node) => {
|
|
136
|
+
const record = S.encodeSync(gitNodeCodec)({
|
|
137
|
+
...node,
|
|
138
|
+
context: S.decodeUnknownSync(workflowRunContextSchema)(node.context),
|
|
139
|
+
});
|
|
140
|
+
owner.sql.exec("INSERT INTO standalone_workflow_git_nodes(idempotency_key,run_id,record) VALUES(?,?,?) ON CONFLICT(idempotency_key) DO UPDATE SET record=excluded.record", idempotencyKey, S.decodeUnknownSync(workflowRunContextSchema)(node.context).runId, record);
|
|
141
|
+
};
|
|
142
|
+
/** The dispatched Git step this call key names, while its run is active. */
|
|
143
|
+
const gitNode = (idempotencyKey) => {
|
|
144
|
+
const row = owner.sql
|
|
145
|
+
.exec("SELECT record FROM standalone_workflow_git_nodes WHERE idempotency_key=?", idempotencyKey)
|
|
146
|
+
.toArray()[0];
|
|
147
|
+
if (row === undefined)
|
|
148
|
+
return null;
|
|
149
|
+
const node = S.decodeUnknownSync(gitNodeCodec)(S.decodeUnknownSync(S.Struct({ record: S.String }))(row).record);
|
|
150
|
+
const run = store.read(node.context.runId);
|
|
151
|
+
return run?.teamId === node.teamId &&
|
|
152
|
+
store.outcome(node.context.runId) === "active"
|
|
153
|
+
? node
|
|
154
|
+
: null;
|
|
155
|
+
};
|
|
156
|
+
owner.sql.exec(`CREATE TABLE IF NOT EXISTS standalone_workflow_catalog (
|
|
157
|
+
installation_id TEXT PRIMARY KEY, release_key TEXT NOT NULL, record TEXT NOT NULL
|
|
158
|
+
) STRICT`);
|
|
159
|
+
const admitted = async (installationId) => {
|
|
160
|
+
const installation = await host.installations.get(installationId);
|
|
161
|
+
if (installation?.status !== "enabled")
|
|
162
|
+
throw new Error("Workflow installation is unavailable");
|
|
163
|
+
const verified = await verifyRelease(host.artifacts, installation.release, undefined, false);
|
|
164
|
+
return { installation, verified };
|
|
165
|
+
};
|
|
166
|
+
const publisherGranted = async (selected) => {
|
|
167
|
+
const declaration = selected.verified.declaration;
|
|
168
|
+
if (declaration === null)
|
|
169
|
+
return false;
|
|
170
|
+
const snapshot = await deriveDeclaredOperations(selected.installation.release, declaration, host.capabilities ?? []);
|
|
171
|
+
return (grantsFor(selected.installation, declaration, snapshot, "workflow.publish") !== null);
|
|
172
|
+
};
|
|
173
|
+
const runGranted = async (selected) => {
|
|
174
|
+
const declaration = selected.verified.declaration;
|
|
175
|
+
if (declaration === null)
|
|
176
|
+
return false;
|
|
177
|
+
const snapshot = await deriveDeclaredOperations(selected.installation.release, declaration, host.capabilities ?? []);
|
|
178
|
+
if (grantsFor(selected.installation, declaration, snapshot, "workflow.run") === null)
|
|
179
|
+
return false;
|
|
180
|
+
return approvedBindingGrants(selected.installation, declaration).some((binding) => binding.binding === "WORKFLOWS" &&
|
|
181
|
+
binding.operations.includes("runGraph"));
|
|
182
|
+
};
|
|
183
|
+
const checkLibraryOperation = (selected, operation) => {
|
|
184
|
+
if (!selected.verified.declaration?.capabilities.includes("workflow.run") ||
|
|
185
|
+
!selected.verified.declaration.backend?.bindings.some((binding) => binding.name === "WORKFLOWS" &&
|
|
186
|
+
binding.operations.includes(operation)))
|
|
187
|
+
throw new Error("Workflow operation is not granted");
|
|
188
|
+
};
|
|
189
|
+
const admitLibrary = async (installationId, operation) => {
|
|
190
|
+
const selected = await admitted(installationId);
|
|
191
|
+
checkLibraryOperation(selected, operation);
|
|
192
|
+
return selected;
|
|
193
|
+
};
|
|
194
|
+
const admitInvocation = async (raw, operation) => {
|
|
195
|
+
const facts = S.decodeUnknownSync(invocationFactsSchema)(raw, {
|
|
196
|
+
onExcessProperty: "error",
|
|
197
|
+
});
|
|
198
|
+
if (facts.subject.kind !== "call" ||
|
|
199
|
+
facts.selection.owner.kind !== "installation")
|
|
200
|
+
throw new AppInstanceError("revoked");
|
|
201
|
+
if (facts.deadlineAt <= Date.now())
|
|
202
|
+
throw new AppInstanceError("expired");
|
|
203
|
+
if (!facts.grants.some((grant) => grant.binding === "WORKFLOWS" && grant.operations.includes(operation)))
|
|
204
|
+
throw new AppInstanceError("revoked");
|
|
205
|
+
const libraryId = facts.selection.owner.id;
|
|
206
|
+
const selected = await admitLibrary(libraryId, operation);
|
|
207
|
+
const current = invocationAuthorityOf(selected.installation, selected.verified);
|
|
208
|
+
if (current.instanceId !== facts.instanceId ||
|
|
209
|
+
current.selection.appId !== facts.selection.appId ||
|
|
210
|
+
current.selection.releaseId !== facts.selection.releaseId ||
|
|
211
|
+
current.selection.authorityVersion !== facts.selection.authorityVersion ||
|
|
212
|
+
current.artifactDigest !== facts.artifactDigest)
|
|
213
|
+
throw new AppInstanceError("revoked");
|
|
214
|
+
if (facts.deadlineAt <= Date.now())
|
|
215
|
+
throw new AppInstanceError("expired");
|
|
216
|
+
return {
|
|
217
|
+
...selected,
|
|
218
|
+
libraryId,
|
|
219
|
+
context: facts.subject.context,
|
|
220
|
+
installationAuthority: {
|
|
221
|
+
selection: current.selection,
|
|
222
|
+
artifactDigest: current.artifactDigest,
|
|
223
|
+
},
|
|
224
|
+
};
|
|
225
|
+
};
|
|
226
|
+
/** A trigger's live library pin; any missing grant is a typed revocation. */
|
|
227
|
+
const triggerAuthority = async (installationId) => {
|
|
228
|
+
const installation = await host.installations.get(installationId);
|
|
229
|
+
if (installation?.status !== "enabled")
|
|
230
|
+
throw new AppInstanceError("revoked");
|
|
231
|
+
const selected = await admitted(installationId);
|
|
232
|
+
if (!(await runGranted(selected)) ||
|
|
233
|
+
!selected.verified.declaration?.capabilities.includes("git.repository"))
|
|
234
|
+
throw new AppInstanceError("revoked");
|
|
235
|
+
const current = invocationAuthorityOf(selected.installation, selected.verified);
|
|
236
|
+
return {
|
|
237
|
+
installation: selected.installation,
|
|
238
|
+
pin: {
|
|
239
|
+
selection: current.selection,
|
|
240
|
+
artifactDigest: current.artifactDigest,
|
|
241
|
+
},
|
|
242
|
+
};
|
|
243
|
+
};
|
|
244
|
+
/** An emitted event fires triggers only as its live Release declares it. */
|
|
245
|
+
const admitEventFact = async (raw) => {
|
|
246
|
+
const fact = appEventFactSchema.parse(raw);
|
|
247
|
+
const { installation, verified } = await admitted(fact.installationId);
|
|
248
|
+
const declared = verified.api?.events?.[fact.type];
|
|
249
|
+
if (installation.release.appId !== fact.appId ||
|
|
250
|
+
installation.release.releaseId !== fact.releaseId ||
|
|
251
|
+
declared === undefined ||
|
|
252
|
+
fact.payload.type !== fact.type ||
|
|
253
|
+
!new Validator(declared.payload, "2020-12").validate(fact.payload).valid)
|
|
254
|
+
throw new AppInstanceError("revoked");
|
|
255
|
+
return fact;
|
|
256
|
+
};
|
|
257
|
+
// Immutable discovery data only. Admission and dispatch never read this copy.
|
|
258
|
+
const projection = async (installation, verifiedLibrary) => {
|
|
259
|
+
const key = releaseKey(installation);
|
|
260
|
+
const row = owner.sql
|
|
261
|
+
.exec("SELECT release_key,record FROM standalone_workflow_catalog WHERE installation_id=?", installation.installationId)
|
|
262
|
+
.toArray()[0];
|
|
263
|
+
if (row !== undefined) {
|
|
264
|
+
const record = S.decodeUnknownSync(catalogRecordSchema)(row);
|
|
265
|
+
if (record.release_key === key)
|
|
266
|
+
return S.decodeUnknownSync(catalogProjectionCodec)(record.record);
|
|
267
|
+
}
|
|
268
|
+
const verified = verifiedLibrary ??
|
|
269
|
+
(await verifyRelease(host.artifacts, installation.release, undefined, false));
|
|
270
|
+
let value = null;
|
|
271
|
+
if (verified.api !== null) {
|
|
272
|
+
value = {
|
|
273
|
+
...(await catalogMethods(verified.api.methods)),
|
|
274
|
+
displayName: verified.declaration?.displayName ?? installation.installationId,
|
|
275
|
+
capabilities: verified.declaration?.capabilities ?? [],
|
|
276
|
+
};
|
|
277
|
+
if (verified.api.events !== undefined)
|
|
278
|
+
value = { ...value, ...(await catalogEvents(verified.api.events)) };
|
|
279
|
+
if (verified.declaration?.icon !== undefined)
|
|
280
|
+
value = { ...value, icon: verified.declaration.icon };
|
|
281
|
+
}
|
|
282
|
+
const record = JSON.stringify(value);
|
|
283
|
+
// A cache ceiling does not restrict the App API: larger projections still return.
|
|
284
|
+
if (new TextEncoder().encode(record).byteLength <= 512 * 1024)
|
|
285
|
+
owner.sql.exec("INSERT INTO standalone_workflow_catalog(installation_id,release_key,record) VALUES(?,?,?) ON CONFLICT(installation_id) DO UPDATE SET release_key=excluded.release_key,record=excluded.record", installation.installationId, key, record);
|
|
286
|
+
return value;
|
|
287
|
+
};
|
|
288
|
+
async function catalogForLibrary(library, scope) {
|
|
289
|
+
checkLibraryOperation(library, "catalog");
|
|
290
|
+
const wanted = scope !== undefined && "installationIds" in scope
|
|
291
|
+
? new Set(scope.installationIds)
|
|
292
|
+
: null;
|
|
293
|
+
const installations = (await host.installations.list()).filter((installation) => installation.status === "enabled" &&
|
|
294
|
+
(wanted === null || wanted.has(installation.installationId)));
|
|
295
|
+
// This is the wire catalog's existing bound, not an installation limit.
|
|
296
|
+
if (installations.length > 200)
|
|
297
|
+
throw new Error("Workflow catalog is too large");
|
|
298
|
+
// Only a full sweep (no filter) ever enumerates every installation, so
|
|
299
|
+
// only a full sweep may prune cache rows for installations no longer
|
|
300
|
+
// present; a narrow filter must never touch other installations' rows.
|
|
301
|
+
if (wanted === null)
|
|
302
|
+
owner.sql.exec("DELETE FROM standalone_workflow_catalog WHERE installation_id NOT IN (SELECT value FROM json_each(?))", JSON.stringify(installations.map((installation) => installation.installationId)));
|
|
303
|
+
const apps = [];
|
|
304
|
+
for (let offset = 0; offset < installations.length; offset += 8) {
|
|
305
|
+
// Settle every sibling before returning, so no read outlives this method.
|
|
306
|
+
const results = await Promise.allSettled(installations.slice(offset, offset + 8).map(async (installation) => {
|
|
307
|
+
const value = await projection(installation, releaseKey(installation) === releaseKey(library.installation)
|
|
308
|
+
? library.verified
|
|
309
|
+
: undefined);
|
|
310
|
+
if (value === null)
|
|
311
|
+
return null;
|
|
312
|
+
let workflowPublishGranted = false;
|
|
313
|
+
let workflowRunGranted = false;
|
|
314
|
+
if (value.capabilities?.includes("workflow.publish") ||
|
|
315
|
+
value.capabilities?.includes("workflow.run")) {
|
|
316
|
+
const current = await host.installations.get(installation.installationId);
|
|
317
|
+
if (current?.status !== "enabled" ||
|
|
318
|
+
releaseKey(current) !== releaseKey(installation))
|
|
319
|
+
throw new AppInstanceError("revoked");
|
|
320
|
+
const selected = releaseKey(current) === releaseKey(library.installation)
|
|
321
|
+
? { installation: current, verified: library.verified }
|
|
322
|
+
: await admitted(current.installationId);
|
|
323
|
+
workflowPublishGranted = await publisherGranted(selected);
|
|
324
|
+
workflowRunGranted = await runGranted(selected);
|
|
325
|
+
const after = await host.installations.get(current.installationId);
|
|
326
|
+
if (after?.status !== "enabled" ||
|
|
327
|
+
after.authorityVersion !== current.authorityVersion ||
|
|
328
|
+
releaseKey(after) !== releaseKey(current))
|
|
329
|
+
throw new AppInstanceError("revoked");
|
|
330
|
+
}
|
|
331
|
+
return {
|
|
332
|
+
...value,
|
|
333
|
+
workflowPublishGranted,
|
|
334
|
+
workflowRunGranted,
|
|
335
|
+
installationId: installation.installationId,
|
|
336
|
+
appId: installation.release.appId,
|
|
337
|
+
};
|
|
338
|
+
}));
|
|
339
|
+
for (const result of results) {
|
|
340
|
+
if (result.status === "rejected")
|
|
341
|
+
throw result.reason;
|
|
342
|
+
if (result.value !== null)
|
|
343
|
+
apps.push(result.value);
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
// Only the index ever crosses the App-reply wire boundary unbounded, so
|
|
347
|
+
// only it can refuse too_large; a typed refusal beats the generic
|
|
348
|
+
// App-reply ceiling's untyped throw when it still grows too large.
|
|
349
|
+
if (scope === undefined) {
|
|
350
|
+
const reply = { apps: apps.map(summarizedEntry) };
|
|
351
|
+
const bytes = new TextEncoder().encode(JSON.stringify(reply)).byteLength;
|
|
352
|
+
return bytes > MAX_APP_REPLY_BYTES
|
|
353
|
+
? { error: "too_large" }
|
|
354
|
+
: reply;
|
|
355
|
+
}
|
|
356
|
+
return { apps };
|
|
357
|
+
}
|
|
358
|
+
const catalog = async (facts, input) => {
|
|
359
|
+
const library = await admitInvocation(facts, "catalog");
|
|
360
|
+
if (input?.installationId === undefined)
|
|
361
|
+
return catalogForLibrary(library);
|
|
362
|
+
const { apps } = await catalogForLibrary(library, {
|
|
363
|
+
installationIds: [input.installationId],
|
|
364
|
+
});
|
|
365
|
+
const [app] = apps;
|
|
366
|
+
if (app === undefined)
|
|
367
|
+
return { error: "not_found" };
|
|
368
|
+
// The single-installation detail is this method's own wire reply, so it
|
|
369
|
+
// alone (of the scoped fetches) still applies the ceiling.
|
|
370
|
+
const reply = { app };
|
|
371
|
+
const bytes = new TextEncoder().encode(JSON.stringify(reply)).byteLength;
|
|
372
|
+
return bytes > MAX_APP_REPLY_BYTES
|
|
373
|
+
? { error: "too_large" }
|
|
374
|
+
: reply;
|
|
375
|
+
};
|
|
376
|
+
return {
|
|
377
|
+
store,
|
|
378
|
+
caller,
|
|
379
|
+
admitLibrary,
|
|
380
|
+
admitInvocation,
|
|
381
|
+
triggerAuthority,
|
|
382
|
+
admitEventFact,
|
|
383
|
+
gitNode,
|
|
384
|
+
/** A finished run's Git steps can no longer be named by a pass mint. */
|
|
385
|
+
forgetGitNodes(runId) {
|
|
386
|
+
owner.sql.exec("DELETE FROM standalone_workflow_git_nodes WHERE run_id=?", runId);
|
|
387
|
+
},
|
|
388
|
+
/** Trigger runs act as the owner who armed them on their pinned Computer. */
|
|
389
|
+
recordTriggerCaller(runId, memberId, computerId) {
|
|
390
|
+
const context = {
|
|
391
|
+
actor: { kind: "member", memberId },
|
|
392
|
+
computerId,
|
|
393
|
+
role: "owner",
|
|
394
|
+
};
|
|
395
|
+
owner.sql.exec("INSERT OR IGNORE INTO standalone_workflow_callers(run_id,caller) VALUES(?,?)", runId, JSON.stringify(context));
|
|
396
|
+
},
|
|
397
|
+
catalog,
|
|
398
|
+
// Host readers use this only after admitting the live member and library.
|
|
399
|
+
catalogForLibrary,
|
|
400
|
+
async prepare(facts, raw) {
|
|
401
|
+
const selected = await admitInvocation(facts, "runGraph");
|
|
402
|
+
const { libraryId, context } = selected;
|
|
403
|
+
const request = S.decodeUnknownSync(graphStartRequestSchema)(raw, {
|
|
404
|
+
onExcessProperty: "error",
|
|
405
|
+
});
|
|
406
|
+
const publication = request.published;
|
|
407
|
+
if (publication !== undefined) {
|
|
408
|
+
if (!(await publisherGranted(selected)))
|
|
409
|
+
return { error: "not_granted" };
|
|
410
|
+
if (publication.digest !==
|
|
411
|
+
(await contentDigest(S.decodeUnknownSync(S.Json)(request.definition))))
|
|
412
|
+
return { error: "invalid_publication" };
|
|
413
|
+
}
|
|
414
|
+
// A team-key caller never becomes a principal: it compiles against its
|
|
415
|
+
// own grants, strictly under the live member's access it borrows. The
|
|
416
|
+
// MCP gate has already required `apps.use` on this Workflows
|
|
417
|
+
// installation, because `runGraph` is a write.
|
|
418
|
+
const keyActor = context.actor.kind === "team-key" ? context.actor : null;
|
|
419
|
+
const key = keyActor === null
|
|
420
|
+
? null
|
|
421
|
+
: ((await host.keys?.live(keyActor.keyId)) ?? null);
|
|
422
|
+
if (keyActor !== null && key === null)
|
|
423
|
+
return { error: "not_granted" };
|
|
424
|
+
// Compile reads only the Apps this request's own bindings name, never
|
|
425
|
+
// every installed App; a key then narrows those to its own grants.
|
|
426
|
+
const boundInstallationIds = [
|
|
427
|
+
...new Set(Object.values(request.bindings).filter((value) => typeof value === "string")),
|
|
428
|
+
];
|
|
429
|
+
const available = await catalogForLibrary(selected, {
|
|
430
|
+
installationIds: boundInstallationIds,
|
|
431
|
+
});
|
|
432
|
+
if ("error" in available)
|
|
433
|
+
return available;
|
|
434
|
+
const entries = key === null ? available.apps : keyCatalogEntries(available.apps, key);
|
|
435
|
+
const compiled = await compileForMember(entries, request);
|
|
436
|
+
if (compiled.kind === "invalid")
|
|
437
|
+
return compiled;
|
|
438
|
+
// This host has no human external-effect approval owner. A Git step needs
|
|
439
|
+
// a published run: its pass names the Workflow and the arming member.
|
|
440
|
+
// A key never gets a Git step either way: the key vocabulary has no
|
|
441
|
+
// Git grant yet.
|
|
442
|
+
const graphs = [
|
|
443
|
+
compiled.plan.definition,
|
|
444
|
+
...Object.values(compiled.plan.definition.subgraphs ?? {}),
|
|
445
|
+
];
|
|
446
|
+
if (graphs.some((graph) => graph.nodes.some((node) => {
|
|
447
|
+
if (node.kind !== "app")
|
|
448
|
+
return false;
|
|
449
|
+
const binding = compiled.plan.bindings[node.app];
|
|
450
|
+
return ((node.gitAccess !== undefined &&
|
|
451
|
+
(key !== null || publication === undefined)) ||
|
|
452
|
+
(binding !== undefined &&
|
|
453
|
+
"appId" in binding &&
|
|
454
|
+
compiled.plan.catalog[binding.appId]?.methods[node.method]
|
|
455
|
+
?.effect === "external"));
|
|
456
|
+
})))
|
|
457
|
+
return { error: "not_granted" };
|
|
458
|
+
const admitted = admitRunInput(compiled.plan.definition, request.input);
|
|
459
|
+
if (admitted.kind === "invalid")
|
|
460
|
+
return { error: "invalid_input" };
|
|
461
|
+
const runId = "run_" +
|
|
462
|
+
(await contentDigest([libraryId, request.requestId])).slice(7, 47);
|
|
463
|
+
// An Agent runs with the access of the person who approved its App
|
|
464
|
+
// tools.
|
|
465
|
+
const subject = facts.subject.kind === "call" ? facts.subject : null;
|
|
466
|
+
const agent = context.actor.kind === "agent" &&
|
|
467
|
+
subject?.agentPolicy !== undefined &&
|
|
468
|
+
subject.sessionId !== undefined
|
|
469
|
+
? {
|
|
470
|
+
kind: "agent",
|
|
471
|
+
agentId: context.actor.agentId,
|
|
472
|
+
sessionId: subject.sessionId,
|
|
473
|
+
grantorMemberId: subject.agentPolicy.grantorMemberId,
|
|
474
|
+
}
|
|
475
|
+
: null;
|
|
476
|
+
// A key's run always acts as its owner, with role owner, the same
|
|
477
|
+
// path an Agent's grantor already takes. The key never becomes a
|
|
478
|
+
// member or an Agent principal on its own request.
|
|
479
|
+
const keyInitiator = keyActor === null || key === null
|
|
480
|
+
? null
|
|
481
|
+
: { kind: "key", keyId: keyActor.keyId, grantorMemberId: key.owner };
|
|
482
|
+
if (context.actor.kind !== "member" &&
|
|
483
|
+
agent === null &&
|
|
484
|
+
keyInitiator === null)
|
|
485
|
+
return { error: "not_granted" };
|
|
486
|
+
const actorId = agent !== null
|
|
487
|
+
? agent.grantorMemberId
|
|
488
|
+
: keyInitiator !== null
|
|
489
|
+
? keyInitiator.grantorMemberId
|
|
490
|
+
: context.actor.kind === "member"
|
|
491
|
+
? context.actor.memberId
|
|
492
|
+
: undefined;
|
|
493
|
+
if (actorId === undefined)
|
|
494
|
+
return { error: "not_granted" };
|
|
495
|
+
// A key has no Agent Session, so it can never declare a delivery.
|
|
496
|
+
if (request.delivery !== undefined &&
|
|
497
|
+
(keyInitiator !== null ||
|
|
498
|
+
!(await options.admitDelivery(actorId, request.delivery))))
|
|
499
|
+
return { error: "not_granted" };
|
|
500
|
+
const preparation = agent !== null
|
|
501
|
+
? {
|
|
502
|
+
initiatedBy: agent,
|
|
503
|
+
source: "agent",
|
|
504
|
+
libraryInstallationId: libraryId,
|
|
505
|
+
computerId: context.computerId,
|
|
506
|
+
plan: compiled.plan,
|
|
507
|
+
}
|
|
508
|
+
: keyInitiator !== null
|
|
509
|
+
? {
|
|
510
|
+
initiatedBy: keyInitiator,
|
|
511
|
+
source: "key",
|
|
512
|
+
libraryInstallationId: libraryId,
|
|
513
|
+
computerId: context.computerId,
|
|
514
|
+
plan: compiled.plan,
|
|
515
|
+
}
|
|
516
|
+
: {
|
|
517
|
+
initiatedBy: { kind: "member", memberId: actorId },
|
|
518
|
+
source: "manual",
|
|
519
|
+
libraryInstallationId: libraryId,
|
|
520
|
+
computerId: context.computerId,
|
|
521
|
+
plan: compiled.plan,
|
|
522
|
+
};
|
|
523
|
+
const captured = await options.authority(libraryId).resolve(preparation);
|
|
524
|
+
if (captured.kind !== "admitted" || captured.actor.memberId !== actorId)
|
|
525
|
+
return { error: "not_granted" };
|
|
526
|
+
// A declared delivery wins; an Agent's own run reports to its Session.
|
|
527
|
+
const delivery = request.delivery ??
|
|
528
|
+
(agent === null
|
|
529
|
+
? undefined
|
|
530
|
+
: { agentId: agent.agentId, sessionId: agent.sessionId });
|
|
531
|
+
const started = {
|
|
532
|
+
teamId: libraryId,
|
|
533
|
+
runId,
|
|
534
|
+
requestId: await contentDigest([libraryId, request.requestId]),
|
|
535
|
+
actorId,
|
|
536
|
+
computerId: context.computerId,
|
|
537
|
+
plan: compiled.plan,
|
|
538
|
+
ceiling: captured.ceiling,
|
|
539
|
+
input: admitted.input,
|
|
540
|
+
source: preparation.source,
|
|
541
|
+
initiatedBy: preparation.initiatedBy,
|
|
542
|
+
};
|
|
543
|
+
const record = delivery === undefined ? started : { ...started, delivery };
|
|
544
|
+
const current = await admitInvocation(facts, "runGraph");
|
|
545
|
+
if (publication !== undefined && !(await publisherGranted(current)))
|
|
546
|
+
return { error: "not_granted" };
|
|
547
|
+
const callContext = {
|
|
548
|
+
actor: captured.actor,
|
|
549
|
+
computerId: context.computerId,
|
|
550
|
+
};
|
|
551
|
+
// Standalone admits only the owner, so an Agent's or a key's grantor
|
|
552
|
+
// acts as owner.
|
|
553
|
+
if (agent !== null || keyInitiator !== null)
|
|
554
|
+
callContext.role = "owner";
|
|
555
|
+
else if (context.role !== undefined)
|
|
556
|
+
callContext.role = context.role;
|
|
557
|
+
const run = publication === undefined
|
|
558
|
+
? record
|
|
559
|
+
: {
|
|
560
|
+
...record,
|
|
561
|
+
publication: {
|
|
562
|
+
installationId: libraryId,
|
|
563
|
+
workflowId: publication.workflowId,
|
|
564
|
+
commit: publication.commit,
|
|
565
|
+
digest: publication.digest,
|
|
566
|
+
armedBy: captured.actor,
|
|
567
|
+
},
|
|
568
|
+
};
|
|
569
|
+
const kind = owner.transactionSync(() => {
|
|
570
|
+
const savedCaller = readCaller(runId);
|
|
571
|
+
if (savedCaller !== null &&
|
|
572
|
+
canonicalJson(S.decodeUnknownSync(S.Json)(savedCaller)) !==
|
|
573
|
+
canonicalJson(S.decodeUnknownSync(S.Json)(callContext)))
|
|
574
|
+
return "conflict";
|
|
575
|
+
// A replay from a different source or initiator fails here too: the
|
|
576
|
+
// store compares the whole stored record, source and initiator included.
|
|
577
|
+
const accepted = store.startCompiled(run);
|
|
578
|
+
if (accepted !== "conflict")
|
|
579
|
+
owner.sql.exec("INSERT OR IGNORE INTO standalone_workflow_callers(run_id,caller) VALUES(?,?)", runId, JSON.stringify(callContext));
|
|
580
|
+
return accepted;
|
|
581
|
+
});
|
|
582
|
+
return { kind, runId, planDigest: compiled.plan.planDigest };
|
|
583
|
+
},
|
|
584
|
+
async loadRun(libraryId, runId) {
|
|
585
|
+
await admitLibrary(libraryId, "runGraph");
|
|
586
|
+
const run = store.read(runId);
|
|
587
|
+
if (run?.teamId !== libraryId || store.outcome(runId) !== "active")
|
|
588
|
+
throw new Error("Workflow run is not active");
|
|
589
|
+
return { plan: run.plan, input: run.input };
|
|
590
|
+
},
|
|
591
|
+
/** Live member, Computer and library run grant, before every node kind. */
|
|
592
|
+
async beforeNode(raw) {
|
|
593
|
+
const event = S.decodeUnknownSync(nodeEventSchema)(raw, {
|
|
594
|
+
onExcessProperty: "error",
|
|
595
|
+
});
|
|
596
|
+
const run = store.read(event.runId);
|
|
597
|
+
if (run === null || store.outcome(event.runId) !== "active")
|
|
598
|
+
return { kind: "refused", code: "revoked" };
|
|
599
|
+
const savedCaller = caller(run.runId);
|
|
600
|
+
if (savedCaller.actor.kind !== "member")
|
|
601
|
+
return { kind: "refused", code: "revoked" };
|
|
602
|
+
const decision = await options
|
|
603
|
+
.authority(run.teamId)
|
|
604
|
+
.recheckRun(preparationFromRun(run, run.teamId));
|
|
605
|
+
if (decision.kind === "refused")
|
|
606
|
+
store.recordEvent({
|
|
607
|
+
...event,
|
|
608
|
+
state: "failed",
|
|
609
|
+
refusal: decision.code,
|
|
610
|
+
errorMessage: decision.code.replaceAll("_", " "),
|
|
611
|
+
});
|
|
612
|
+
return decision;
|
|
613
|
+
},
|
|
614
|
+
async dispatch(call) {
|
|
615
|
+
const run = store.read(call.runId);
|
|
616
|
+
if (run?.teamId !== call.teamId || store.outcome(call.runId) !== "active")
|
|
617
|
+
return { kind: "refused", code: "revoked" };
|
|
618
|
+
await admitLibrary(run.teamId, "runGraph");
|
|
619
|
+
const resolved = planNodeCall(run, call);
|
|
620
|
+
if (resolved === null)
|
|
621
|
+
return { kind: "refused", code: "not_granted" };
|
|
622
|
+
const pinned = run.plan.catalog[resolved.appId]?.methods[resolved.method];
|
|
623
|
+
const { installation, verified } = await admitted(call.installationId);
|
|
624
|
+
if (verified.api === null ||
|
|
625
|
+
installation.release.appId !== resolved.appId)
|
|
626
|
+
return { kind: "refused", code: "revoked" };
|
|
627
|
+
const current = (await catalogMethods(verified.api.methods)).methods[resolved.method];
|
|
628
|
+
if (!pinned ||
|
|
629
|
+
current?.digest !== pinned.digest ||
|
|
630
|
+
pinned.effect === "external")
|
|
631
|
+
return { kind: "refused", code: "revoked" };
|
|
632
|
+
if (store.outcome(call.runId) !== "active")
|
|
633
|
+
return { kind: "refused", code: "revoked" };
|
|
634
|
+
const savedCaller = caller(run.runId);
|
|
635
|
+
if (savedCaller.actor.kind !== "member")
|
|
636
|
+
return { kind: "refused", code: "revoked" };
|
|
637
|
+
const live = options.authority(run.teamId);
|
|
638
|
+
await live.member(savedCaller.actor.memberId);
|
|
639
|
+
if (run.computerId !== null &&
|
|
640
|
+
!(await live.memberComputer(savedCaller.actor.memberId, run.computerId)))
|
|
641
|
+
return { kind: "refused", code: "revoked" };
|
|
642
|
+
const currentAuthority = invocationAuthorityOf(installation, verified);
|
|
643
|
+
const authority = {
|
|
644
|
+
...currentAuthority,
|
|
645
|
+
// A binding this node's ceiling leaves no operation on is not granted.
|
|
646
|
+
grants: currentAuthority.grants
|
|
647
|
+
.map((grant) => ({
|
|
648
|
+
...grant,
|
|
649
|
+
operations: grant.operations.filter((operation) => resolved.bindingCeiling.some((binding) => binding.name === grant.binding &&
|
|
650
|
+
binding.operations.includes(operation))),
|
|
651
|
+
}))
|
|
652
|
+
.filter((grant) => grant.operations.length > 0),
|
|
653
|
+
};
|
|
654
|
+
const deadlineAt = Date.now() + APP_INVOCATION_CEILING_MS;
|
|
655
|
+
const deadline = { deadlineAt, grantExpiresAt: deadlineAt };
|
|
656
|
+
const facts = run.publication === undefined
|
|
657
|
+
? invocationFacts.fromCaller(authority, {
|
|
658
|
+
...savedCaller,
|
|
659
|
+
idempotencyKey: call.idempotencyKey,
|
|
660
|
+
}, deadline)
|
|
661
|
+
: invocationFacts.fromWorkflowNode(authority, {
|
|
662
|
+
...savedCaller,
|
|
663
|
+
workflow: {
|
|
664
|
+
workflowId: run.publication.workflowId,
|
|
665
|
+
runId: run.runId,
|
|
666
|
+
nodeId: resolved.nodeId,
|
|
667
|
+
armedBy: savedCaller.actor,
|
|
668
|
+
},
|
|
669
|
+
idempotencyKey: call.idempotencyKey,
|
|
670
|
+
}, deadline);
|
|
671
|
+
if (resolved.gitAccess !== undefined)
|
|
672
|
+
recordGitNode(call.idempotencyKey, {
|
|
673
|
+
teamId: run.teamId,
|
|
674
|
+
memberId: savedCaller.actor.memberId,
|
|
675
|
+
installationId: resolved.installationId,
|
|
676
|
+
appId: resolved.appId,
|
|
677
|
+
method: resolved.method,
|
|
678
|
+
methodDigest: resolved.methodDigest,
|
|
679
|
+
computerId: run.computerId,
|
|
680
|
+
context: resolved,
|
|
681
|
+
});
|
|
682
|
+
const result = await host.instances
|
|
683
|
+
.get(authority.instanceId)
|
|
684
|
+
.call(facts, verified.api, resolved.method, resolved.input);
|
|
685
|
+
if (store.outcome(call.runId) !== "active")
|
|
686
|
+
return { kind: "refused", code: "revoked" };
|
|
687
|
+
if (!result.ok) {
|
|
688
|
+
// InstanceResult's own producer vocabulary (packages/oyster) names
|
|
689
|
+
// every structural/authority rejection as an AppInstanceErrorCode;
|
|
690
|
+
// "failed" is its sole catch-all for an unclassified backend
|
|
691
|
+
// failure. Only a code that is actually a live decision about
|
|
692
|
+
// today's grant refuses the node as "revoked" (or, for a binding
|
|
693
|
+
// the Release never granted, "not_granted"). Every execution or
|
|
694
|
+
// availability code -- a deadline "expired" included -- throws, so
|
|
695
|
+
// the graph records a failed step and the engine's own retry
|
|
696
|
+
// applies, never a silent "revoked" that misreports a timeout or a
|
|
697
|
+
// transient unavailability as an authority decision.
|
|
698
|
+
switch (result.code) {
|
|
699
|
+
// A moved authority version is itself the live decision, not a
|
|
700
|
+
// retryable fault; same for an instance the owner state marks
|
|
701
|
+
// inactive.
|
|
702
|
+
case "revoked":
|
|
703
|
+
case "instance_owner_mismatch":
|
|
704
|
+
case "instance_authority_version_mismatch":
|
|
705
|
+
case "instance_release_mismatch":
|
|
706
|
+
case "release_no_longer_matches_instance":
|
|
707
|
+
case "installation_tenant_mismatch":
|
|
708
|
+
case "stale_authority":
|
|
709
|
+
case "instance_inactive":
|
|
710
|
+
return { kind: "refused", code: "revoked" };
|
|
711
|
+
case "binding_not_granted":
|
|
712
|
+
return { kind: "refused", code: "not_granted" };
|
|
713
|
+
case "failed":
|
|
714
|
+
throw new Error(result.causeClass === undefined
|
|
715
|
+
? "App execution failed"
|
|
716
|
+
: `App execution failed (${result.causeClass})`);
|
|
717
|
+
case "expired":
|
|
718
|
+
case "binding_unavailable":
|
|
719
|
+
case "reentrant":
|
|
720
|
+
case "conflicting_owner_state":
|
|
721
|
+
case "invalid_owner_state":
|
|
722
|
+
case "invalid_invocation_facts":
|
|
723
|
+
case "instance_identity_mismatch":
|
|
724
|
+
case "instance_deleted":
|
|
725
|
+
case "release_app_mismatch":
|
|
726
|
+
case "state_schema_migration_unsupported":
|
|
727
|
+
throw new Error(`App execution failed (${result.code})`);
|
|
728
|
+
default: {
|
|
729
|
+
const exhaustive = result.code;
|
|
730
|
+
throw new Error(`App execution failed (${String(exhaustive)})`);
|
|
731
|
+
}
|
|
732
|
+
}
|
|
733
|
+
}
|
|
734
|
+
// The engine's closed refusal vocabulary names host decisions; an App's
|
|
735
|
+
// own domain refusal fails the node with its code.
|
|
736
|
+
if (result.value.kind === "refused")
|
|
737
|
+
throw new Error(`App method refused (${result.value.code})`);
|
|
738
|
+
const value = result.value.value;
|
|
739
|
+
const pending = pinned.pending === true ? pendingSchema.safeParse(value) : null;
|
|
740
|
+
if (pending?.success)
|
|
741
|
+
return pending.data;
|
|
742
|
+
if (!accepts(parseProjection(pinned.output), value))
|
|
743
|
+
return { kind: "refused", code: "invalid_output" };
|
|
744
|
+
return {
|
|
745
|
+
kind: "complete",
|
|
746
|
+
output: S.decodeUnknownSync(S.Json)(value),
|
|
747
|
+
costMilliunits: 0,
|
|
748
|
+
};
|
|
749
|
+
},
|
|
750
|
+
};
|
|
751
|
+
}
|