@fungi.computer/braid 0.1.0 → 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.
@@ -12,6 +12,8 @@ const failureCode = (failure) => {
12
12
  return "guest_exception";
13
13
  case "DeadlineExceeded":
14
14
  return "deadline_exceeded";
15
+ case "SandboxBusy":
16
+ return "sandbox_failed";
15
17
  case "Cancelled":
16
18
  return "cancelled";
17
19
  case "ResultNotJson":
@@ -1,4 +1,4 @@
1
- import { z } from "zod";
1
+ import * as z from "zod";
2
2
  /** Frozen installation identity or explicit registered Team-source namespace. */
3
3
  export declare const workflowBindingSchema: z.ZodUnion<readonly [z.ZodObject<{
4
4
  appId: z.ZodString;
@@ -1,4 +1,4 @@
1
- import { z } from "zod";
1
+ import * as z from "zod";
2
2
  import { repositoryRefSchema, repositoryRefsEqual, } from "@fungi.computer/agar/git";
3
3
  const repository = z
4
4
  .string()
@@ -1,5 +1,26 @@
1
1
  import { z } from "zod";
2
2
  import type { GitWriter } from "@fungi.computer/agar/git";
3
+ /** One credential grammar for both hosts; the owner is public, the token is not. */
4
+ export declare const workflowGitPassPrefix = "wgp.";
5
+ export declare const workflowGitPassTokenSchema: z.ZodPipe<z.ZodString, z.ZodTransform<{
6
+ ownerId: string;
7
+ token: string;
8
+ }, string>>;
9
+ /** Format the existing standalone bearer shape, never a credential-bearing URL. */
10
+ export declare function formatWorkflowGitPassToken(ownerId: string, token: string): string;
11
+ /** Returned once to the native dispatcher, never to an App or a durable record. */
12
+ export type WorkflowGitCredential = Readonly<{
13
+ bearer: string;
14
+ remote: string;
15
+ expiresAt: number;
16
+ }>;
17
+ /** Git reads this command-local config without writing a credential or repository config. */
18
+ export declare function workflowGitExecutionEnvironment(credential: WorkflowGitCredential): {
19
+ FUNGI_GIT_REMOTE: string;
20
+ GIT_CONFIG_COUNT: string;
21
+ GIT_CONFIG_KEY_0: string;
22
+ GIT_CONFIG_VALUE_0: string;
23
+ };
3
24
  /** Non-secret Team-issued authority for one stored Workflow step on its pinned Computer. */
4
25
  export declare const workflowGitPassGrantSchema: z.ZodLazy<z.ZodObject<{
5
26
  teamId: z.ZodString;
@@ -2,6 +2,30 @@ import { z } from "zod";
2
2
  import { workflowAttestationSchema } from "@fungi.computer/puffball/app-method-runtime";
3
3
  import { gitAccessScope, resolvedGitAccessSchema } from "../git-access.js";
4
4
  const id = z.string().min(1).max(256);
5
+ /** One credential grammar for both hosts; the owner is public, the token is not. */
6
+ export const workflowGitPassPrefix = "wgp.";
7
+ export const workflowGitPassTokenSchema = z
8
+ .string()
9
+ .regex(/^wgp\.([a-zA-Z0-9_-]{1,256})\.([0-9a-f]{64})$/u)
10
+ .transform((bearer) => {
11
+ const [, ownerId = "", token = ""] = bearer.split(".");
12
+ return { ownerId, token };
13
+ });
14
+ /** Format the existing standalone bearer shape, never a credential-bearing URL. */
15
+ export function formatWorkflowGitPassToken(ownerId, token) {
16
+ const bearer = `${workflowGitPassPrefix}${ownerId}.${token}`;
17
+ workflowGitPassTokenSchema.parse(bearer);
18
+ return bearer;
19
+ }
20
+ /** Git reads this command-local config without writing a credential or repository config. */
21
+ export function workflowGitExecutionEnvironment(credential) {
22
+ return {
23
+ FUNGI_GIT_REMOTE: credential.remote,
24
+ GIT_CONFIG_COUNT: "1",
25
+ GIT_CONFIG_KEY_0: `http.${credential.remote}.extraHeader`,
26
+ GIT_CONFIG_VALUE_0: `Authorization: Bearer ${credential.bearer}`,
27
+ };
28
+ }
5
29
  /** Non-secret Team-issued authority for one stored Workflow step on its pinned Computer. */
6
30
  export const workflowGitPassGrantSchema = z.lazy(() => z
7
31
  .strictObject({
@@ -1,32 +1,35 @@
1
1
  import { type RepositoryRef } from "@fungi.computer/agar/git";
2
2
  import type { WatchdogSqliteOwner } from "@fungi.computer/watchdog";
3
- import { type WorkflowGitPassGrant } from "./git-pass-contract.js";
4
- /** Scoped pass state shared by both Computer hosts; credentials never enter the grant. */
3
+ import { type WorkflowGitCredential, type WorkflowGitPassGrant } from "./git-pass-contract.js";
4
+ /** One fresh credential per newly admitted native invocation; only its digest is durable. */
5
5
  export declare function createWorkflowGitPassStore(options: Readonly<{
6
6
  sql: WatchdogSqliteOwner["sql"];
7
7
  transactionSync: <T>(body: () => T) => T;
8
8
  ownerId(): string | null;
9
9
  computerId(): string | undefined;
10
- mintPassId(): string;
11
- remote(passId: string, repository: RepositoryRef): string;
10
+ mintToken?: () => string;
11
+ remote(repository: RepositoryRef): string;
12
12
  denied(): Error;
13
13
  now?: () => number;
14
14
  }>): Readonly<{
15
- openWorkflowPass(raw: WorkflowGitPassGrant): {
16
- passId: string;
17
- remote: string;
18
- expiresAt: number;
19
- bearer?: string;
20
- };
15
+ /** Only the NEW native-admission path calls this; replay never obtains another secret. */
16
+ issue(rawInvocationId: string, rawGrant: WorkflowGitPassGrant): Promise<WorkflowGitCredential>;
17
+ /** A live native replay can extend its existing lease, never issue or resurrect it. */
18
+ renew(rawInvocationId: string, rawGrant: WorkflowGitPassGrant): void;
19
+ /** Keep the public receipt so a delayed issuer cannot reopen the same invocation. */
20
+ close(rawInvocationId: string, rawGrant: WorkflowGitPassGrant): void;
21
21
  /** Retain a tombstone so a delayed mint cannot resurrect a terminal run. */
22
22
  revokeWorkflowRunGitPasses(input: {
23
23
  teamId: string;
24
24
  runId: string;
25
25
  }): void;
26
- /** Re-admit the stored scope for every Git request; an expired pass grants nothing. */
26
+ /** Every request also needs the host's live native-invocation and domain admission. */
27
27
  admit(input: {
28
- passId: string;
28
+ bearer: string;
29
29
  repository: RepositoryRef;
30
30
  scope: "read" | "write";
31
- }): WorkflowGitPassGrant | null;
31
+ }): Promise<{
32
+ invocationId: string;
33
+ grant: WorkflowGitPassGrant;
34
+ } | null>;
32
35
  }>;
@@ -1,55 +1,124 @@
1
1
  import { repositoryRefsEqual, } from "@fungi.computer/agar/git";
2
- import { workflowGitPassResultSchema } from "@fungi.computer/agar/computer-contracts";
3
2
  import { z } from "zod";
4
- import { workflowGitPassGrantSchema, workflowGitPassRevocationSchema, } from "./git-pass-contract.js";
5
- /** Scoped pass state shared by both Computer hosts; credentials never enter the grant. */
3
+ import { canonicalJson } from "../index.js";
4
+ import { formatWorkflowGitPassToken, workflowGitPassTokenSchema, workflowGitPassGrantSchema, workflowGitPassRevocationSchema, } from "./git-pass-contract.js";
5
+ const invocationIdSchema = z.string().min(1).max(256);
6
+ const tokenSchema = z.string().regex(/^[0-9a-f]{64}$/u);
7
+ const leaseSchema = z.strictObject({
8
+ invocation_id: invocationIdSchema,
9
+ run_id: z.string(),
10
+ grant_json: z.string(),
11
+ token_digest: z.string().nullable(),
12
+ expires_at: z.number().int(),
13
+ state: z.enum(["open", "closed"]),
14
+ });
15
+ const remoteSchema = z
16
+ .url({ protocol: /^https$/, abort: true })
17
+ .refine((value) => {
18
+ const url = new URL(value);
19
+ return url.username === "" && url.password === "";
20
+ }, "Workflow Git remotes must not contain credentials");
21
+ function mintToken() {
22
+ return Array.from(crypto.getRandomValues(new Uint8Array(32)), (byte) => byte.toString(16).padStart(2, "0")).join("");
23
+ }
24
+ /** A verifier digest, never a native invocation identity or replay hash. */
25
+ async function tokenDigest(token) {
26
+ const digest = await crypto.subtle.digest("SHA-256", new TextEncoder().encode(token));
27
+ return Array.from(new Uint8Array(digest), (byte) => byte.toString(16).padStart(2, "0")).join("");
28
+ }
29
+ /** Optional internal fields use JSON's omission semantics at the storage boundary. */
30
+ function serializeGrant(grant) {
31
+ return canonicalJson(z.json().parse(JSON.parse(JSON.stringify(grant))));
32
+ }
33
+ /** One fresh credential per newly admitted native invocation; only its digest is durable. */
6
34
  export function createWorkflowGitPassStore(options) {
7
35
  const { sql } = options;
8
36
  const now = options.now ?? Date.now;
9
- sql.exec(`CREATE TABLE IF NOT EXISTS computer_workflow_git_passes (
10
- pass_id TEXT PRIMARY KEY, node_key TEXT NOT NULL UNIQUE, run_id TEXT NOT NULL,
11
- grant_json TEXT NOT NULL, expires_at INTEGER NOT NULL
37
+ // Pre-release reusable, plaintext pass state is disposable, not another reader.
38
+ sql.exec("DROP TABLE IF EXISTS computer_workflow_git_passes");
39
+ sql.exec(`CREATE TABLE IF NOT EXISTS computer_workflow_git_invocations (
40
+ invocation_id TEXT PRIMARY KEY, run_id TEXT NOT NULL,
41
+ grant_json TEXT NOT NULL, token_digest TEXT UNIQUE,
42
+ expires_at INTEGER NOT NULL,
43
+ state TEXT NOT NULL CHECK (state IN ('open', 'closed'))
12
44
  ) STRICT`);
13
45
  sql.exec(`CREATE TABLE IF NOT EXISTS computer_workflow_git_ended_runs (run_id TEXT PRIMARY KEY) STRICT`);
46
+ function admittedGrant(raw) {
47
+ const grant = workflowGitPassGrantSchema.parse(raw);
48
+ if (grant.teamId !== options.ownerId() ||
49
+ grant.computerId !== options.computerId() ||
50
+ grant.workflow.runId !== grant.runId ||
51
+ grant.workflow.nodeId !== grant.nodeId ||
52
+ grant.workflow.armedBy.kind !== "member" ||
53
+ grant.workflow.armedBy.memberId !== grant.memberId)
54
+ throw options.denied();
55
+ return grant;
56
+ }
57
+ function ended(runId) {
58
+ return (sql
59
+ .exec("SELECT 1 FROM computer_workflow_git_ended_runs WHERE run_id=?", runId)
60
+ .toArray().length !== 0);
61
+ }
62
+ function read(invocationId) {
63
+ const row = sql
64
+ .exec("SELECT * FROM computer_workflow_git_invocations WHERE invocation_id=?", invocationId)
65
+ .toArray()[0];
66
+ return row === undefined ? undefined : leaseSchema.parse(row);
67
+ }
14
68
  return Object.freeze({
15
- openWorkflowPass(raw) {
16
- const grant = workflowGitPassGrantSchema.parse(raw);
17
- if (grant.teamId !== options.ownerId() ||
18
- grant.computerId !== options.computerId() ||
19
- grant.workflow.runId !== grant.runId ||
20
- grant.workflow.nodeId !== grant.nodeId ||
21
- grant.workflow.armedBy.kind !== "member" ||
22
- grant.workflow.armedBy.memberId !== grant.memberId)
69
+ /** Only the NEW native-admission path calls this; replay never obtains another secret. */
70
+ async issue(rawInvocationId, rawGrant) {
71
+ const invocationId = invocationIdSchema.parse(rawInvocationId);
72
+ const grant = admittedGrant(rawGrant);
73
+ if (ended(grant.runId) || read(invocationId) !== undefined)
23
74
  throw options.denied();
75
+ const remote = remoteSchema.parse(options.remote(grant.gitAccess.repository));
76
+ const token = tokenSchema.parse((options.mintToken ?? mintToken)());
77
+ const bearer = formatWorkflowGitPassToken(grant.teamId, token);
78
+ const digest = await tokenDigest(token);
24
79
  return options.transactionSync(() => {
25
- if (sql
26
- .exec("SELECT 1 FROM computer_workflow_git_ended_runs WHERE run_id=?", grant.runId)
27
- .toArray().length > 0)
28
- throw options.denied();
29
- const key = JSON.stringify([
30
- grant.runId,
31
- grant.graphId ?? null,
32
- grant.nodeId,
33
- grant.iteration,
34
- grant.scope,
35
- ]);
36
- const row = sql
37
- .exec("SELECT pass_id,grant_json,expires_at FROM computer_workflow_git_passes WHERE node_key=?", key)
38
- .toArray()[0];
39
- if (row !== undefined &&
40
- z.string().parse(row.grant_json) !== JSON.stringify(grant))
80
+ // The digest await can race a terminal revocation or another issuer.
81
+ admittedGrant(grant);
82
+ if (ended(grant.runId) || read(invocationId) !== undefined)
41
83
  throw options.denied();
42
- const passId = row === undefined
43
- ? options.mintPassId()
44
- : z.string().parse(row.pass_id);
45
84
  const expiresAt = now() + 60_000;
46
- sql.exec(`INSERT INTO computer_workflow_git_passes VALUES(?,?,?,?,?)
47
- ON CONFLICT(node_key) DO UPDATE SET expires_at=excluded.expires_at`, passId, key, grant.runId, JSON.stringify(grant), expiresAt);
48
- return workflowGitPassResultSchema.parse({
49
- passId,
50
- expiresAt,
51
- remote: options.remote(passId, grant.gitAccess.repository),
52
- });
85
+ sql.exec("INSERT INTO computer_workflow_git_invocations VALUES(?,?,?,?,?,'open')", invocationId, grant.runId, serializeGrant(grant), digest, expiresAt);
86
+ return { bearer, remote, expiresAt };
87
+ });
88
+ },
89
+ /** A live native replay can extend its existing lease, never issue or resurrect it. */
90
+ renew(rawInvocationId, rawGrant) {
91
+ const invocationId = invocationIdSchema.parse(rawInvocationId);
92
+ const grant = admittedGrant(rawGrant);
93
+ options.transactionSync(() => {
94
+ const row = read(invocationId);
95
+ if (row?.state !== "open" ||
96
+ row.expires_at <= now() ||
97
+ row.grant_json !== serializeGrant(grant) ||
98
+ ended(grant.runId))
99
+ throw options.denied();
100
+ sql.exec("UPDATE computer_workflow_git_invocations SET expires_at=? WHERE invocation_id=?", now() + 60_000, invocationId);
101
+ });
102
+ },
103
+ /** Keep the public receipt so a delayed issuer cannot reopen the same invocation. */
104
+ close(rawInvocationId, rawGrant) {
105
+ const invocationId = invocationIdSchema.parse(rawInvocationId);
106
+ options.transactionSync(() => {
107
+ const row = read(invocationId);
108
+ if (row !== undefined) {
109
+ // Revocation never depends on current authority or reconstructed
110
+ // metadata still being sufficient to admit a new command.
111
+ sql.exec("UPDATE computer_workflow_git_invocations SET state='closed',token_digest=NULL WHERE invocation_id=?", invocationId);
112
+ return;
113
+ }
114
+ const grant = workflowGitPassGrantSchema.parse(rawGrant);
115
+ // The run tombstone already prevents delayed issuance. Do not recreate
116
+ // receipts after terminal cleanup has deleted the run's invocation rows.
117
+ if (ended(grant.runId))
118
+ return;
119
+ // Close can beat the issue RPC or its digest await. Retain the same
120
+ // public identity either way, so neither can publish a later credential.
121
+ sql.exec("INSERT INTO computer_workflow_git_invocations VALUES(?,?,?,NULL,0,'closed')", invocationId, grant.runId, serializeGrant(grant));
53
122
  });
54
123
  },
55
124
  /** Retain a tombstone so a delayed mint cannot resurrect a terminal run. */
@@ -59,24 +128,31 @@ export function createWorkflowGitPassStore(options) {
59
128
  throw options.denied();
60
129
  options.transactionSync(() => {
61
130
  sql.exec("INSERT OR IGNORE INTO computer_workflow_git_ended_runs VALUES(?)", runId);
62
- sql.exec("DELETE FROM computer_workflow_git_passes WHERE run_id=?", runId);
131
+ sql.exec("DELETE FROM computer_workflow_git_invocations WHERE run_id=?", runId);
63
132
  });
64
133
  },
65
- /** Re-admit the stored scope for every Git request; an expired pass grants nothing. */
66
- admit(input) {
67
- const at = now();
68
- const row = sql
69
- .exec("SELECT grant_json,expires_at FROM computer_workflow_git_passes WHERE pass_id=?", input.passId)
134
+ /** Every request also needs the host's live native-invocation and domain admission. */
135
+ async admit(input) {
136
+ const parsed = workflowGitPassTokenSchema.safeParse(input.bearer);
137
+ if (!parsed.success || parsed.data.ownerId !== options.ownerId())
138
+ return null;
139
+ const digest = await tokenDigest(parsed.data.token);
140
+ const raw = sql
141
+ .exec("SELECT * FROM computer_workflow_git_invocations WHERE token_digest=?", digest)
70
142
  .toArray()[0];
71
- if (row === undefined || z.number().parse(row.expires_at) <= at)
143
+ if (raw === undefined)
144
+ return null;
145
+ const row = leaseSchema.parse(raw);
146
+ if (row.state !== "open" || row.expires_at <= now())
72
147
  return null;
73
- const grant = workflowGitPassGrantSchema.parse(JSON.parse(z.string().parse(row.grant_json)));
148
+ const grant = workflowGitPassGrantSchema.parse(JSON.parse(row.grant_json));
74
149
  if (grant.teamId !== options.ownerId() ||
75
150
  grant.computerId !== options.computerId() ||
76
151
  !repositoryRefsEqual(grant.gitAccess.repository, input.repository) ||
77
- (input.scope === "write" && grant.scope !== "write"))
152
+ (input.scope === "write" && grant.scope !== "write") ||
153
+ ended(grant.runId))
78
154
  return null;
79
- return grant;
155
+ return { invocationId: row.invocation_id, grant };
80
156
  },
81
157
  });
82
158
  }
@@ -1,5 +1,4 @@
1
1
  import type { RepositoryRef } from "@fungi.computer/agar/git";
2
- import { type WorkflowGitPassResult } from "@fungi.computer/agar/computer-contracts";
3
2
  import type { WorkflowRunContext } from "./graph-contract.js";
4
3
  import type { StoredWorkflowNode } from "./graph-dispatch.js";
5
4
  import { type WorkflowGitPassGrant } from "./git-pass-contract.js";
@@ -19,7 +18,6 @@ export declare function createWorkflowGitPasses(ports: Readonly<{
19
18
  method: string;
20
19
  }): WorkflowGitPassNode | null;
21
20
  admitMemberGitAccess(memberId: string, repository: RepositoryRef, scope: WorkflowGitScope): Promise<void>;
22
- open(computerId: string, grant: WorkflowGitPassGrant): Promise<WorkflowGitPassResult>;
23
21
  revoke(computerId: string, input: {
24
22
  teamId: string;
25
23
  runId: string;
@@ -33,9 +31,10 @@ export declare function createWorkflowGitPasses(ports: Readonly<{
33
31
  wake?(due: number | null): Promise<void>;
34
32
  }>): {
35
33
  cleanupWake: () => number | null;
36
- openWorkflowPass(context: WorkflowRunContext, target: {
34
+ /** Returns only public authority; native NEW admission owns credential issuance. */
35
+ admitWorkflowGit(context: WorkflowRunContext, target: {
37
36
  installationId: string;
38
37
  method: string;
39
- }): Promise<WorkflowGitPassResult>;
38
+ }): Promise<WorkflowGitPassGrant>;
40
39
  deliverCleanups(): Promise<void>;
41
40
  };
@@ -1,4 +1,3 @@
1
- import { workflowGitPassResultSchema, } from "@fungi.computer/agar/computer-contracts";
2
1
  import { canonicalJson } from "../index.js";
3
2
  import { gitAccessScope } from "../git-access.js";
4
3
  import { workflowGitPassGrantSchema, } from "./git-pass-contract.js";
@@ -7,7 +6,8 @@ export function createWorkflowGitPasses(ports) {
7
6
  const cleanupWake = () => ports.pending().length === 0 ? null : Date.now() + 5000;
8
7
  return {
9
8
  cleanupWake,
10
- async openWorkflowPass(context, target) {
9
+ /** Returns only public authority; native NEW admission owns credential issuance. */
10
+ async admitWorkflowGit(context, target) {
11
11
  const admitted = ports.admitNode(context, target);
12
12
  if (admitted?.computerId == null ||
13
13
  admitted.context.gitAccess === undefined ||
@@ -38,21 +38,7 @@ export function createWorkflowGitPasses(ports) {
38
38
  if (canonicalJson(ports.admitNode(context, target)) !==
39
39
  canonicalJson(admitted))
40
40
  throw ports.denied();
41
- const pass = workflowGitPassResultSchema.parse(await ports.open(grant.computerId, grant));
42
- try {
43
- await ports.admitMemberGitAccess(grant.memberId, grant.gitAccess.repository, scope);
44
- if (canonicalJson(ports.admitNode(context, target)) !==
45
- canonicalJson(admitted))
46
- throw ports.denied();
47
- }
48
- catch (cause) {
49
- await ports.revoke(grant.computerId, {
50
- teamId: grant.teamId,
51
- runId: grant.runId,
52
- });
53
- throw cause;
54
- }
55
- return pass;
41
+ return grant;
56
42
  },
57
43
  async deliverCleanups() {
58
44
  try {
@@ -416,7 +416,7 @@ export declare const graphRunSchema: S.Struct<{
416
416
  readonly graphId: S.optionalKey<S.String>;
417
417
  readonly nodeId: S.String;
418
418
  readonly level: S.Literals<readonly ["read", "write"]>;
419
- readonly capabilities: S.$Array<S.Literals<readonly ["agent.list", "agent.manage", "agent.rename", "agent.avatar", "agent.toolPolicy", "agent.read", "agent.send", "computer.list", "computer.readFile", "computer.writeFile", "computer.execute", "llm.generation", "media.generation", "games:servers", "git.repository", "apps.open", "apps.call", "app.storage", "app.events.emit", "team.workbenchHost", "marketplace.browse", "marketplace.purchase", "workflow.run", "workflow.publish", "workflow.arm", "world", "channels.read", "channels.personal.manage"]>>;
419
+ readonly capabilities: S.$Array<S.Literals<readonly ["approvals", "agent.list", "agent.manage", "agent.rename", "agent.avatar", "agent.toolPolicy", "agent.read", "agent.send", "computer.list", "computer.readFile", "computer.writeFile", "computer.execute", "llm.generation", "media.generation", "games:servers", "git.repository", "apps.open", "apps.call", "app.storage", "app.events.emit", "team.workbenchHost", "marketplace.browse", "marketplace.purchase", "workflow.run", "workflow.publish", "workflow.arm", "world", "channels.read", "channels.personal.manage"]>>;
420
420
  readonly bindings: S.$Array<S.Union<S.Struct<{
421
421
  readonly name: S.Literal<"BACKGROUND" | "GIT" | "EVENTS" | "APPS" | "WORKFLOWS" | "APP_STORAGE" | "MEDIA" | "COMPUTER" | "INBOX" | "AGENTS">;
422
422
  readonly operations: S.$Array<S.Literals<string[]>>;
@@ -145,10 +145,17 @@ export type WorkflowTriggerRunPorts = Readonly<{
145
145
  startCachedGraph(binding: import("./trigger-owner.js").ArmedWorkflowTrigger, request: Pick<import("./graph-start.js").GraphStartRequest, "requestId" | "input"> & {
146
146
  /** The firing event's repository, evaluated from the armed trigger. */
147
147
  triggerRepository?: string;
148
- }): Promise<Exclude<GraphStartReply, {
148
+ },
149
+ /** Host-private binding check, evaluated with the durable run write. */
150
+ admission?: WorkflowTriggerAdmission): Promise<Exclude<GraphStartReply, {
149
151
  kind: "invalid";
150
152
  }>>;
151
153
  }>;
154
+ /** Host-private checks and reservation identity, never supplied by a sender. */
155
+ export type WorkflowTriggerAdmission = Readonly<{
156
+ admitBinding(): boolean;
157
+ reservationId?: string;
158
+ }>;
152
159
  /** Hosts retain their existing error vocabulary and denial classification. */
153
160
  export type WorkflowErrorPort = Readonly<{
154
161
  denied(): Error;
@@ -51,7 +51,7 @@ export declare const runCeilingSchema: S.Struct<{
51
51
  readonly graphId: S.optionalKey<S.String>;
52
52
  readonly nodeId: S.String;
53
53
  readonly level: S.Literals<readonly ["read", "write"]>;
54
- readonly capabilities: S.$Array<S.Literals<readonly ["agent.list", "agent.manage", "agent.rename", "agent.avatar", "agent.toolPolicy", "agent.read", "agent.send", "computer.list", "computer.readFile", "computer.writeFile", "computer.execute", "llm.generation", "media.generation", "games:servers", "git.repository", "apps.open", "apps.call", "app.storage", "app.events.emit", "team.workbenchHost", "marketplace.browse", "marketplace.purchase", "workflow.run", "workflow.publish", "workflow.arm", "world", "channels.read", "channels.personal.manage"]>>;
54
+ readonly capabilities: S.$Array<S.Literals<readonly ["approvals", "agent.list", "agent.manage", "agent.rename", "agent.avatar", "agent.toolPolicy", "agent.read", "agent.send", "computer.list", "computer.readFile", "computer.writeFile", "computer.execute", "llm.generation", "media.generation", "games:servers", "git.repository", "apps.open", "apps.call", "app.storage", "app.events.emit", "team.workbenchHost", "marketplace.browse", "marketplace.purchase", "workflow.run", "workflow.publish", "workflow.arm", "world", "channels.read", "channels.personal.manage"]>>;
55
55
  readonly bindings: S.$Array<S.Union<S.Struct<{
56
56
  readonly name: S.Literal<"BACKGROUND" | "GIT" | "EVENTS" | "APPS" | "WORKFLOWS" | "APP_STORAGE" | "MEDIA" | "COMPUTER" | "INBOX" | "AGENTS">;
57
57
  readonly operations: S.$Array<S.Literals<string[]>>;
@@ -1,11 +1,12 @@
1
1
  import * as S from "effect/Schema";
2
2
  import { type NodeEvent, type NodeAdmissionResult } from "../execution.js";
3
+ import { z } from "zod";
3
4
  import type { WatchdogSqliteOwner } from "@fungi.computer/watchdog";
4
5
  import { type WorkflowRunAuthority } from "./run-authority.js";
5
6
  import type { ArmedWorkflowTrigger } from "./trigger-owner.js";
6
7
  import { type WorkflowRunContext, type GraphRun, type WorkflowRunDelivery } from "./graph-contract.js";
7
8
  import { type StoredWorkflowNode } from "./graph-dispatch.js";
8
- import type { WorkflowAppCall, WorkflowAppCallResult, WorkflowCatalogEntry } from "./ports.js";
9
+ import type { WorkflowAppCall, WorkflowAppCallResult, WorkflowCatalogEntry, WorkflowTriggerAdmission } from "./ports.js";
9
10
  import { type GraphStartRequest } from "./graph-start.js";
10
11
  /** The member can supply a plan, never stored authority or a captured ceiling. */
11
12
  export declare const memberCompiledGraphStartSchema: S.Struct<{
@@ -301,6 +302,8 @@ export declare function createTeamWorkflowRuns(options: {
301
302
  sql: WatchdogSqliteOwner["sql"];
302
303
  transactionSync: <T>(body: () => T) => T;
303
304
  teamId: () => string;
305
+ /** Host-owned wall clock; deterministic native journeys may supply their clock. */
306
+ now?: () => number;
304
307
  admission: () => "active" | "suspended";
305
308
  errors: Readonly<{
306
309
  app(code: "denied" | "invalid" | "unavailable"): Error;
@@ -655,7 +658,7 @@ export declare function createTeamWorkflowRuns(options: {
655
658
  readonly operations: readonly string[];
656
659
  }[];
657
660
  readonly nodeId: string;
658
- readonly capabilities: readonly ("agent.list" | "agent.manage" | "agent.rename" | "agent.avatar" | "agent.toolPolicy" | "agent.read" | "agent.send" | "computer.list" | "computer.readFile" | "computer.writeFile" | "computer.execute" | "llm.generation" | "media.generation" | "games:servers" | "git.repository" | "apps.open" | "apps.call" | "app.storage" | "app.events.emit" | "team.workbenchHost" | "marketplace.browse" | "marketplace.purchase" | "workflow.run" | "workflow.publish" | "workflow.arm" | "world" | "channels.read" | "channels.personal.manage")[];
661
+ readonly capabilities: readonly ("approvals" | "agent.list" | "agent.manage" | "agent.rename" | "agent.avatar" | "agent.toolPolicy" | "agent.read" | "agent.send" | "computer.list" | "computer.readFile" | "computer.writeFile" | "computer.execute" | "llm.generation" | "media.generation" | "games:servers" | "git.repository" | "apps.open" | "apps.call" | "app.storage" | "app.events.emit" | "team.workbenchHost" | "marketplace.browse" | "marketplace.purchase" | "workflow.run" | "workflow.publish" | "workflow.arm" | "world" | "channels.read" | "channels.personal.manage")[];
659
662
  readonly graphId?: string;
660
663
  }[];
661
664
  readonly nodeGitAccess: readonly {
@@ -1555,7 +1558,7 @@ export declare function createTeamWorkflowRuns(options: {
1555
1558
  }>;
1556
1559
  startCachedGraph(binding: ArmedWorkflowTrigger, started: Pick<GraphStartRequest, "requestId" | "input"> & {
1557
1560
  triggerRepository?: string;
1558
- }): Promise<{
1561
+ }, admission?: WorkflowTriggerAdmission): Promise<{
1559
1562
  kind: "conflict" | "started" | "replay";
1560
1563
  runId: string;
1561
1564
  planDigest: string;
@@ -1581,6 +1584,66 @@ export declare function createTeamWorkflowRuns(options: {
1581
1584
  revokeReleaseInMutation(appIds: readonly string[]): void;
1582
1585
  /** The kernel's lifecycle caller owns the transaction. Suspension ends every run. */
1583
1586
  onLifecycle(status: "active" | "suspended"): void;
1587
+ reserveWebhookDelivery(raw: z.input<z.ZodObject<{
1588
+ scope: z.ZodLazy<z.ZodObject<{
1589
+ installationId: z.ZodString;
1590
+ workflowId: z.ZodString;
1591
+ commit: z.ZodString;
1592
+ triggerId: z.ZodString;
1593
+ }, z.core.$strict>>;
1594
+ delivery: z.ZodObject<{
1595
+ host: z.ZodString;
1596
+ tenant: z.ZodString;
1597
+ bindingId: z.ZodString;
1598
+ eventId: z.ZodString;
1599
+ }, z.core.$strict>;
1600
+ cargo: z.ZodJSONSchema;
1601
+ limits: z.ZodObject<{
1602
+ hostConcurrency: z.ZodNumber;
1603
+ bindingConcurrency: z.ZodNumber;
1604
+ }, z.core.$strict>;
1605
+ }, z.core.$strict>>): {
1606
+ kind: "reserved" | "replay";
1607
+ reservationId: string;
1608
+ runId: string | null;
1609
+ } | {
1610
+ kind: "refused";
1611
+ reason: "limit";
1612
+ };
1613
+ pendingWebhookDeliveries(rawOwner: z.input<z.ZodObject<{
1614
+ host: z.ZodString;
1615
+ tenant: z.ZodString;
1616
+ }, z.core.$strict>>): ({
1617
+ kind: "launch";
1618
+ reservationId: string;
1619
+ runId: string;
1620
+ } | {
1621
+ scope: {
1622
+ installationId: string;
1623
+ workflowId: string;
1624
+ commit: string;
1625
+ triggerId: string;
1626
+ };
1627
+ delivery: {
1628
+ host: string;
1629
+ tenant: string;
1630
+ bindingId: string;
1631
+ eventId: string;
1632
+ };
1633
+ cargo: z.core.util.JSONType;
1634
+ limits: {
1635
+ hostConcurrency: number;
1636
+ bindingConcurrency: number;
1637
+ };
1638
+ kind: "start";
1639
+ reservationId: string;
1640
+ runId?: never;
1641
+ })[];
1642
+ nextWebhookWake(rawOwner: z.input<z.ZodObject<{
1643
+ host: z.ZodString;
1644
+ tenant: z.ZodString;
1645
+ }, z.core.$strict>>): number | null;
1646
+ completeWebhookDelivery(rawReservationId: string): void;
1584
1647
  reserveTriggerRun(raw: {
1585
1648
  scope: import("./trigger-run-reservations.js").TriggerRunScope;
1586
1649
  requestId: string;
@@ -1599,7 +1662,7 @@ export declare function createTeamWorkflowRuns(options: {
1599
1662
  list(filter: {
1600
1663
  installationId: string;
1601
1664
  actorId?: string;
1602
- workflowId? /** A report names its Workflows library, so only a library run may declare one. */: string;
1665
+ workflowId?: string;
1603
1666
  }): readonly (GraphRun & {
1604
1667
  startedAt: number;
1605
1668
  })[];
@@ -384,7 +384,7 @@ export function createTeamWorkflowRuns(options) {
384
384
  throw options.errors.app("denied");
385
385
  return startCompiled(preparation, request, identity, ceiling, undefined, publication, libraryInstallationId);
386
386
  },
387
- async startCachedGraph(binding, started) {
387
+ async startCachedGraph(binding, started, admission) {
388
388
  const { triggerRepository, ...request } = started;
389
389
  await admitGraphMember(binding.armedBy, binding.computerId);
390
390
  const stored = triggerPlans.read(binding);
@@ -467,15 +467,30 @@ export function createTeamWorkflowRuns(options) {
467
467
  trigger = { ...trigger, nodeGitAccess: binding.nodeGitAccess };
468
468
  if (triggerRepository !== undefined)
469
469
  trigger = { ...trigger, repository: triggerRepository };
470
- return startCompiled(preparation, binding.delivery === undefined
471
- ? request
472
- : { ...request, delivery: binding.delivery }, identity, ceiling, trigger, {
473
- installationId: binding.installationId,
474
- workflowId: binding.workflowId,
475
- commit: binding.commit,
476
- digest: binding.digest,
477
- armedBy: { kind: "member", memberId: binding.armedBy },
478
- }, binding.installationId);
470
+ return options.transactionSync(() => {
471
+ // Different external bindings may share the compiled publication.
472
+ // Check the binding's owning store after all capture I/O, in the same
473
+ // transaction as the durable start, rather than invalidating its peers.
474
+ if (admission !== undefined && !admission.admitBinding())
475
+ return {
476
+ kind: "refused",
477
+ reason: "Trigger binding is no longer armed",
478
+ };
479
+ const result = startCompiled(preparation, binding.delivery === undefined
480
+ ? request
481
+ : { ...request, delivery: binding.delivery }, identity, ceiling, trigger, {
482
+ installationId: binding.installationId,
483
+ workflowId: binding.workflowId,
484
+ commit: binding.commit,
485
+ digest: binding.digest,
486
+ armedBy: { kind: "member", memberId: binding.armedBy },
487
+ }, binding.installationId);
488
+ if (admission?.reservationId !== undefined &&
489
+ (result.kind === "started" || result.kind === "replay") &&
490
+ triggerReservations.bindTriggerRun(admission.reservationId, result.runId) === "conflict")
491
+ throw new Error("Trigger reservation binding conflict");
492
+ return result;
493
+ });
479
494
  },
480
495
  callWorkflowApp: options.callWorkflowApp,
481
496
  admitDelivery: options.admitDelivery,
@@ -24,6 +24,7 @@ export declare function createWorkflowSchedules(options: {
24
24
  }>;
25
25
  list(principal: WorkflowTriggerPrincipal, installationId: string): Promise<{
26
26
  scheduleId: string;
27
+ installationId: string;
27
28
  workflowId: string;
28
29
  triggerId: string;
29
30
  nextDueAt: number;
@@ -95,6 +95,7 @@ export function createWorkflowSchedules(options) {
95
95
  .filter((record) => record.installationId === installationId)
96
96
  .map((record) => ({
97
97
  scheduleId: record.id,
98
+ installationId: record.installationId,
98
99
  workflowId: record.workflowId,
99
100
  triggerId: record.triggerId,
100
101
  nextDueAt: record.nextDueAt,
@@ -3,7 +3,7 @@ import * as S from "effect/Schema";
3
3
  import { type GitBinding, type RepositoryRef } from "@fungi.computer/agar/git";
4
4
  import { type TriggerEvent } from "../index.js";
5
5
  import { type WorkflowArmingAuthorityPorts, type WorkflowArmingReceipt } from "./arming-authority.js";
6
- import type { PublishedDefinitionPort, WorkflowPublicationContext, WorkflowTriggerRunPorts, WorkflowErrorPort } from "./ports.js";
6
+ import type { PublishedDefinitionPort, WorkflowPublicationContext, WorkflowTriggerRunPorts, WorkflowErrorPort, WorkflowTriggerAdmission } from "./ports.js";
7
7
  export declare const workflowTriggerTargetSchema: z.ZodObject<{
8
8
  installationId: z.ZodString;
9
9
  workflowId: z.ZodString;
@@ -581,7 +581,7 @@ export declare function createTeamWorkflowTriggers(options: {
581
581
  invalidateInstallation: (installationId: string) => void;
582
582
  /** The single native push owner supplies host-issued Git, never a fire caller. */
583
583
  revalidatePublished(installationId: string, git: GitBinding): Promise<void>;
584
- fire(raw: ArmedWorkflowTrigger, scope: string, event: TriggerEvent): Promise<Readonly<{
584
+ fire(raw: ArmedWorkflowTrigger, scope: string, event: TriggerEvent, admission?: WorkflowTriggerAdmission): Promise<Readonly<{
585
585
  kind: "ignored";
586
586
  }> | Readonly<{
587
587
  kind: "started" | "replay" | "conflict";
@@ -213,7 +213,7 @@ export function createTeamWorkflowTriggers(options) {
213
213
  options.runs.triggerPlans.refuse(binding, "commit no longer published");
214
214
  }
215
215
  },
216
- async fire(raw, scope, event) {
216
+ async fire(raw, scope, event, admission) {
217
217
  const binding = armedWorkflowTriggerSchema.parse(raw);
218
218
  try {
219
219
  const current = await options.admit(binding.armedBy, binding.installationId, binding.computerId);
@@ -256,7 +256,7 @@ export function createTeamWorkflowTriggers(options) {
256
256
  };
257
257
  const result = await options.runs.startCachedGraph(binding, mapped.repository === undefined
258
258
  ? request
259
- : { ...request, triggerRepository: mapped.repository });
259
+ : { ...request, triggerRepository: mapped.repository }, admission);
260
260
  if (result.kind === "refused")
261
261
  return result;
262
262
  if (result.kind === "conflict")
@@ -7,8 +7,72 @@ declare const triggerRunScopeSchema: z.ZodLazy<z.ZodObject<{
7
7
  triggerId: z.ZodString;
8
8
  }, z.core.$strict>>;
9
9
  export type TriggerRunScope = z.output<typeof triggerRunScopeSchema>;
10
+ declare const deliveryInputSchema: z.ZodObject<{
11
+ scope: z.ZodLazy<z.ZodObject<{
12
+ installationId: z.ZodString;
13
+ workflowId: z.ZodString;
14
+ commit: z.ZodString;
15
+ triggerId: z.ZodString;
16
+ }, z.core.$strict>>;
17
+ delivery: z.ZodObject<{
18
+ host: z.ZodString;
19
+ tenant: z.ZodString;
20
+ bindingId: z.ZodString;
21
+ eventId: z.ZodString;
22
+ }, z.core.$strict>;
23
+ cargo: z.ZodJSONSchema;
24
+ limits: z.ZodObject<{
25
+ hostConcurrency: z.ZodNumber;
26
+ bindingConcurrency: z.ZodNumber;
27
+ }, z.core.$strict>;
28
+ }, z.core.$strict>;
29
+ declare const deliveryOwnerSchema: z.ZodObject<{
30
+ host: z.ZodString;
31
+ tenant: z.ZodString;
32
+ }, z.core.$strict>;
10
33
  /** The enclosing Team owner supplies SQLite; callers never select the Team. */
11
- export declare function createTriggerRunReservations(options: Pick<WatchdogSqliteOwner, "sql" | "transactionSync">): {
34
+ export declare function createTriggerRunReservations(options: Pick<WatchdogSqliteOwner, "sql" | "transactionSync"> & {
35
+ now?: () => number;
36
+ }): {
37
+ /** Opaque verified cargo and delivery identity live with the run reservation. */
38
+ reserveWebhookDelivery(raw: z.input<typeof deliveryInputSchema>): {
39
+ kind: "reserved" | "replay";
40
+ reservationId: string;
41
+ runId: string | null;
42
+ } | {
43
+ kind: "refused";
44
+ reason: "limit";
45
+ };
46
+ /** Bounded restart/lost-launch-reply recovery over the SAME reservations. */
47
+ pendingWebhookDeliveries(rawOwner: z.input<typeof deliveryOwnerSchema>): ({
48
+ kind: "launch";
49
+ reservationId: string;
50
+ runId: string;
51
+ } | {
52
+ scope: {
53
+ installationId: string;
54
+ workflowId: string;
55
+ commit: string;
56
+ triggerId: string;
57
+ };
58
+ delivery: {
59
+ host: string;
60
+ tenant: string;
61
+ bindingId: string;
62
+ eventId: string;
63
+ };
64
+ cargo: z.core.util.JSONType;
65
+ limits: {
66
+ hostConcurrency: number;
67
+ bindingConcurrency: number;
68
+ };
69
+ kind: "start";
70
+ reservationId: string;
71
+ runId?: never;
72
+ })[];
73
+ /** Native alarms share this deadline with schedules, including idle TTL cleanup. */
74
+ nextWebhookWake(rawOwner: z.input<typeof deliveryOwnerSchema>): number | null;
75
+ completeWebhookDelivery(rawReservationId: string): void;
12
76
  reserveTriggerRun(raw: {
13
77
  scope: TriggerRunScope;
14
78
  requestId: string;
@@ -33,15 +33,134 @@ const scopeKey = (scope) => {
33
33
  parsed.triggerId,
34
34
  ]);
35
35
  };
36
+ const deliveryIdentitySchema = z.strictObject({
37
+ host: identity,
38
+ tenant: identity,
39
+ bindingId: identity,
40
+ eventId: identity,
41
+ });
42
+ const deliveryInputSchema = z.strictObject({
43
+ scope: triggerRunScopeSchema,
44
+ delivery: deliveryIdentitySchema,
45
+ cargo: z.json(),
46
+ limits: z.strictObject({
47
+ hostConcurrency: z.number().int().positive().max(20),
48
+ bindingConcurrency: z.number().int().positive().max(20),
49
+ }),
50
+ });
51
+ const deliveryOwnerSchema = deliveryIdentitySchema.pick({
52
+ host: true,
53
+ tenant: true,
54
+ });
55
+ const pendingDeliverySchema = rowSchema.and(z.object({
56
+ scope: z.string(),
57
+ delivery_record: z.string().nullable(),
58
+ }));
59
+ const DAY = 24 * 60 * 60 * 1000;
36
60
  /** The enclosing Team owner supplies SQLite; callers never select the Team. */
37
61
  export function createTriggerRunReservations(options) {
38
62
  const { sql } = options;
63
+ const now = options.now ?? Date.now;
39
64
  sql.exec(`CREATE TABLE IF NOT EXISTS hub_trigger_run_reservations (
40
65
  reservation_id TEXT PRIMARY KEY, scope TEXT NOT NULL, request_id TEXT NOT NULL,
41
66
  run_id TEXT UNIQUE, status TEXT NOT NULL CHECK(status IN ('active','released','terminal')),
67
+ delivery_key TEXT, delivery_record TEXT, delivery_expires_at INTEGER,
68
+ delivery_host TEXT, delivery_tenant TEXT, delivery_binding TEXT,
69
+ delivery_pending INTEGER NOT NULL DEFAULT 0 CHECK(delivery_pending IN (0,1)),
42
70
  UNIQUE(scope,request_id)
43
71
  ) STRICT`);
72
+ sql.exec(`CREATE INDEX IF NOT EXISTS hub_trigger_delivery_identity
73
+ ON hub_trigger_run_reservations(delivery_key,delivery_expires_at)`);
74
+ sql.exec(`CREATE INDEX IF NOT EXISTS hub_trigger_delivery_expiry
75
+ ON hub_trigger_run_reservations(delivery_expires_at)`);
76
+ const expireDeliveries = () => {
77
+ const at = now();
78
+ // An expired unstarted intent cannot wake forever. A recorded active run
79
+ // still owns its concurrency slot even after delivery deduplication expires.
80
+ sql.exec("UPDATE hub_trigger_run_reservations SET status='terminal',delivery_pending=0 WHERE reservation_id IN (SELECT reservation_id FROM hub_trigger_run_reservations WHERE delivery_expires_at<=? AND run_id IS NULL LIMIT 100)", at);
81
+ sql.exec("DELETE FROM hub_trigger_run_reservations WHERE reservation_id IN (SELECT reservation_id FROM hub_trigger_run_reservations WHERE delivery_expires_at<=? AND status!='active' AND delivery_pending=0 LIMIT 100)", at);
82
+ sql.exec("UPDATE hub_trigger_run_reservations SET delivery_key=NULL,delivery_record=NULL,delivery_expires_at=NULL WHERE reservation_id IN (SELECT reservation_id FROM hub_trigger_run_reservations WHERE delivery_expires_at<=? AND run_id IS NOT NULL LIMIT 100)", at);
83
+ sql.exec("DELETE FROM hub_trigger_run_reservations WHERE reservation_id IN (SELECT reservation_id FROM hub_trigger_run_reservations WHERE delivery_host IS NOT NULL AND delivery_expires_at IS NULL AND status!='active' AND delivery_pending=0 LIMIT 100)");
84
+ };
44
85
  return {
86
+ /** Opaque verified cargo and delivery identity live with the run reservation. */
87
+ reserveWebhookDelivery(raw) {
88
+ const input = deliveryInputSchema.parse(raw);
89
+ const key = JSON.stringify(input.delivery);
90
+ return options.transactionSync(() => {
91
+ expireDeliveries();
92
+ const prior = sql
93
+ .exec("SELECT reservation_id,run_id,status FROM hub_trigger_run_reservations WHERE delivery_key=? AND delivery_expires_at>? ORDER BY delivery_expires_at DESC LIMIT 1", key, now())
94
+ .toArray()[0];
95
+ if (prior !== undefined) {
96
+ const row = rowSchema.parse(prior);
97
+ return {
98
+ kind: "replay",
99
+ reservationId: row.reservation_id,
100
+ runId: row.run_id,
101
+ };
102
+ }
103
+ const active = z
104
+ .object({
105
+ count: z.number(),
106
+ binding_count: z.number(),
107
+ })
108
+ .parse(sql
109
+ .exec("SELECT count(*) AS count,coalesce(sum(delivery_host=? AND delivery_tenant=? AND delivery_binding=?),0) AS binding_count FROM hub_trigger_run_reservations WHERE status='active'", input.delivery.host, input.delivery.tenant, input.delivery.bindingId)
110
+ .toArray()[0]);
111
+ if (active.count >= input.limits.hostConcurrency ||
112
+ active.binding_count >= input.limits.bindingConcurrency)
113
+ return { kind: "refused", reason: "limit" };
114
+ const reservationId = reservationIds.reservation.generate();
115
+ sql.exec("INSERT INTO hub_trigger_run_reservations(reservation_id,scope,request_id,run_id,status,delivery_key,delivery_record,delivery_expires_at,delivery_pending,delivery_host,delivery_tenant,delivery_binding) VALUES(?,?,?,NULL,'active',?,?,?,1,?,?,?)", reservationId, scopeKey(input.scope), reservationId, key, JSON.stringify(input), now() + DAY, input.delivery.host, input.delivery.tenant, input.delivery.bindingId);
116
+ return { kind: "reserved", reservationId, runId: null };
117
+ });
118
+ },
119
+ /** Bounded restart/lost-launch-reply recovery over the SAME reservations. */
120
+ pendingWebhookDeliveries(rawOwner) {
121
+ const owner = deliveryOwnerSchema.parse(rawOwner);
122
+ return options.transactionSync(() => {
123
+ expireDeliveries();
124
+ return sql
125
+ .exec("SELECT reservation_id,run_id,status,scope,delivery_record FROM hub_trigger_run_reservations WHERE delivery_host=? AND delivery_tenant=? AND delivery_pending=1 ORDER BY delivery_expires_at LIMIT 32", owner.host, owner.tenant)
126
+ .toArray()
127
+ .map((raw) => {
128
+ const row = pendingDeliverySchema.parse(raw);
129
+ if (row.run_id !== null)
130
+ return {
131
+ kind: "launch",
132
+ reservationId: row.reservation_id,
133
+ runId: row.run_id,
134
+ };
135
+ const record = deliveryInputSchema.parse(JSON.parse(z.string().parse(row.delivery_record)));
136
+ return {
137
+ kind: "start",
138
+ reservationId: row.reservation_id,
139
+ ...record,
140
+ };
141
+ });
142
+ });
143
+ },
144
+ /** Native alarms share this deadline with schedules, including idle TTL cleanup. */
145
+ nextWebhookWake(rawOwner) {
146
+ const owner = deliveryOwnerSchema.parse(rawOwner);
147
+ return options.transactionSync(() => {
148
+ expireDeliveries();
149
+ const row = z
150
+ .object({
151
+ pending: z.number(),
152
+ expiry: z.number().nullable(),
153
+ })
154
+ .parse(sql
155
+ .exec("SELECT coalesce(max(delivery_pending),0) AS pending,min(delivery_expires_at) AS expiry FROM hub_trigger_run_reservations WHERE delivery_host=? AND delivery_tenant=?", owner.host, owner.tenant)
156
+ .toArray()[0]);
157
+ return row.pending === 1 ? now() + 1000 : row.expiry;
158
+ });
159
+ },
160
+ completeWebhookDelivery(rawReservationId) {
161
+ const reservationId = identity.parse(rawReservationId);
162
+ sql.exec("UPDATE hub_trigger_run_reservations SET delivery_pending=0,delivery_record=NULL,status=CASE WHEN run_id IS NULL THEN 'terminal' ELSE status END WHERE reservation_id=? AND delivery_pending=1", reservationId);
163
+ },
45
164
  reserveTriggerRun(raw) {
46
165
  const input = reservationInputSchema.parse(raw);
47
166
  const scope = scopeKey(input.scope);
@@ -1,3 +1,3 @@
1
- import { z } from "zod";
1
+ import * as z from "zod";
2
2
  /** Identity of a versioned Workflow document, shared by hosts and the Trellis App. */
3
3
  export declare const workflowIdSchema: z.ZodString;
@@ -1,3 +1,3 @@
1
- import { z } from "zod";
1
+ import * as z from "zod";
2
2
  /** Identity of a versioned Workflow document, shared by hosts and the Trellis App. */
3
3
  export const workflowIdSchema = z.string().regex(/^[a-z0-9][a-z0-9-]{0,62}$/);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fungi.computer/braid",
3
- "version": "0.1.0",
3
+ "version": "0.2.0",
4
4
  "author": "Levi <levi@technomancy.dev>",
5
5
  "type": "module",
6
6
  "description": "Pure versioned Workflow graph validation and content-addressed compilation",
@@ -29,17 +29,17 @@
29
29
  ],
30
30
  "dependencies": {
31
31
  "@cfworker/json-schema": "4.1.1",
32
- "@fungi.computer/almanac": "0.1.0",
33
- "@fungi.computer/mycelium": "0.1.0",
32
+ "@fungi.computer/almanac": "0.2.0",
33
+ "@fungi.computer/mycelium": "0.2.0",
34
34
  "effect": "4.0.2",
35
35
  "jsonpointer": "5.0.1",
36
36
  "mustache": "4.2.0",
37
37
  "zod": "4.4.3",
38
- "@fungi.computer/agar": "0.2.0",
39
- "@fungi.computer/gills": "0.1.0",
40
- "@fungi.computer/watchdog": "0.0.1",
41
- "@fungi.computer/puffball": "0.1.0",
42
- "@fungi.computer/cairn": "0.0.1"
38
+ "@fungi.computer/agar": "0.3.0",
39
+ "@fungi.computer/gills": "0.2.0",
40
+ "@fungi.computer/watchdog": "0.1.0",
41
+ "@fungi.computer/puffball": "0.2.0",
42
+ "@fungi.computer/cairn": "0.1.0"
43
43
  },
44
44
  "devDependencies": {
45
45
  "@cloudflare/workers-types": "5.20260828.1",