@fungi.computer/trellis 0.0.0-stage → 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,90 @@
1
+ import { triggerTargetSchema, scheduleArmReplySchema, triggerDisarmReplySchema, schedulesReplySchema, scheduleDisarmInputSchema, eventDisarmInputSchema, triggerListInputSchema, eventArmReplySchema, eventTriggersReplySchema, } from "@fungi.computer/workflows-wire";
2
+ import { defineAppMethod } from "@fungi.computer/agar/backend-definition";
3
+ import { createWorkflowLibrary } from "./library.js";
4
+ import { readWorkflowCatalog } from "./catalog.js";
5
+ const granted = (context) => {
6
+ if (!context.env.WORKFLOWS)
7
+ throw new Error("Workflow access is not granted");
8
+ return context.env.WORKFLOWS;
9
+ };
10
+ /** Read only this App's immutable publication rows; no Git repair occurs in arming. */
11
+ function publication(context, target) {
12
+ if (context.indexes === undefined)
13
+ return null;
14
+ return createWorkflowLibrary({
15
+ index: context.indexes,
16
+ author: { name: "Trellis", email: "trellis@app.local" },
17
+ catalog: async (appIds) => {
18
+ const catalog = await readWorkflowCatalog(context.env.WORKFLOWS, appIds);
19
+ if (catalog === null)
20
+ throw new Error("Workflow catalog unavailable");
21
+ return catalog;
22
+ },
23
+ }).publishedRow(target.workflowId, { commit: target.commit });
24
+ }
25
+ const unavailablePublication = {
26
+ kind: "refused",
27
+ reason: "Workflow publication is unavailable; re-arm after publishing",
28
+ };
29
+ export const scheduleMethods = {
30
+ arm: defineAppMethod({
31
+ description: "Arm a published Workflow schedule as you on the pinned Computer. An optional delivery chat receives every run's ending.",
32
+ effect: "write",
33
+ requires: ["workflow.run", "workflow.arm"],
34
+ input: triggerTargetSchema,
35
+ output: scheduleArmReplySchema,
36
+ handler: async (input, context) => {
37
+ const workflows = granted(context);
38
+ const stored = publication(context, input);
39
+ if (stored === null)
40
+ return unavailablePublication;
41
+ return scheduleArmReplySchema.parse(await workflows.armSchedule({ ...input, digest: stored.digest }));
42
+ },
43
+ }),
44
+ disarm: defineAppMethod({
45
+ description: "Cancel an armed Workflow schedule.",
46
+ effect: "write",
47
+ requires: ["workflow.run", "workflow.arm"],
48
+ input: scheduleDisarmInputSchema,
49
+ output: triggerDisarmReplySchema,
50
+ handler: async (input, context) => triggerDisarmReplySchema.parse(await granted(context).disarmSchedule(input)),
51
+ }),
52
+ listTriggers: defineAppMethod({
53
+ description: "List this installation's schedules, next run times and paused reasons.",
54
+ effect: "read",
55
+ requires: ["workflow.run"],
56
+ input: triggerListInputSchema,
57
+ output: schedulesReplySchema,
58
+ handler: async (_input, context) => schedulesReplySchema.parse(await granted(context).schedules({})),
59
+ }),
60
+ armEventTrigger: defineAppMethod({
61
+ description: "Arm a published Workflow App-event trigger as you on the pinned Computer. An optional delivery chat receives every run's ending.",
62
+ effect: "write",
63
+ requires: ["workflow.run", "workflow.arm"],
64
+ input: triggerTargetSchema,
65
+ output: eventArmReplySchema,
66
+ handler: async (input, context) => {
67
+ const workflows = granted(context);
68
+ const stored = publication(context, input);
69
+ if (stored === null)
70
+ return unavailablePublication;
71
+ return eventArmReplySchema.parse(await workflows.armEventTrigger({ ...input, digest: stored.digest }));
72
+ },
73
+ }),
74
+ disarmEventTrigger: defineAppMethod({
75
+ description: "Disarm this installation's App-event trigger.",
76
+ effect: "write",
77
+ requires: ["workflow.run", "workflow.arm"],
78
+ input: eventDisarmInputSchema,
79
+ output: triggerDisarmReplySchema,
80
+ handler: async (input, context) => triggerDisarmReplySchema.parse(await granted(context).disarmEventTrigger(input)),
81
+ }),
82
+ eventTriggers: defineAppMethod({
83
+ description: "List this installation's App-event triggers and paused reasons.",
84
+ effect: "read",
85
+ requires: ["workflow.run"],
86
+ input: triggerListInputSchema,
87
+ output: eventTriggersReplySchema,
88
+ handler: async (_input, context) => eventTriggersReplySchema.parse(await granted(context).eventTriggers({})),
89
+ }),
90
+ };
@@ -0,0 +1,140 @@
1
+ import { workflowIdSchema } from "@fungi.computer/braid";
2
+ import * as S from "effect/Schema";
3
+ import * as Result from "effect/Result";
4
+ import { gitSha1Schema } from "@fungi.computer/puffball/git-representation";
5
+ import { sha256DigestSchema } from "@fungi.computer/puffball/effect";
6
+ import { workflowDefinitionSchema, validate, } from "@fungi.computer/braid";
7
+ import { provenanceSchema, shareReview, shareCopyReply, } from "@fungi.computer/workflows-wire";
8
+ const workflowId = S.declare((value) => workflowIdSchema.safeParse(value).success);
9
+ const commit = S.declare((value) => gitSha1Schema.safeParse(value).success);
10
+ export const shareBundleSchema = S.Struct({
11
+ format: S.Literal("fungi.workflow-share/v1"),
12
+ definition: workflowDefinitionSchema,
13
+ layout: S.Record(S.String, S.Struct({ x: S.Finite, y: S.Finite })),
14
+ source: S.Struct({
15
+ workflowId,
16
+ version: S.Number.pipe(S.check(S.isInt()), S.check(S.isGreaterThan(0))),
17
+ commit,
18
+ origin: S.optionalKey(S.String.pipe(S.check(S.isMaxLength(256)))),
19
+ }),
20
+ });
21
+ const maxShareBytes = 512 * 1024;
22
+ function review(definition, catalog) {
23
+ const graphs = [
24
+ { id: "root", graph: definition },
25
+ ...Object.entries(definition.subgraphs ?? {}).map(([id, graph]) => ({
26
+ id,
27
+ graph,
28
+ })),
29
+ ];
30
+ return shareReview.parse({
31
+ gitAccess: graphs.flatMap(({ id, graph }) => graph.nodes.flatMap((node) => node.kind === "app" && node.gitAccess
32
+ ? [{ graph: id, node: node.id, ...node.gitAccess }]
33
+ : [])),
34
+ writeNodes: graphs.flatMap(({ id, graph }) => graph.nodes.flatMap((node) => {
35
+ if (node.kind !== "app")
36
+ return [];
37
+ const appId = definition.apps[node.app]?.appId;
38
+ const method = appId === undefined
39
+ ? undefined
40
+ : catalog[appId]?.methods[node.method];
41
+ return method?.effect === "write" || method?.effect === "external"
42
+ ? [
43
+ {
44
+ graph: id,
45
+ node: node.id,
46
+ appId,
47
+ method: node.method,
48
+ effect: method.effect,
49
+ },
50
+ ]
51
+ : [];
52
+ })),
53
+ triggers: definition.triggers ?? [],
54
+ });
55
+ }
56
+ /** A copy is named after its source; an over-long title keeps the source's. */
57
+ function copyTitled(definition) {
58
+ const named = { ...definition, title: `${definition.title} (copy)` };
59
+ return S.is(workflowDefinitionSchema)(named) ? named : definition;
60
+ }
61
+ async function digest(bytes) {
62
+ const hash = new Uint8Array(await crypto.subtle.digest("SHA-256", Uint8Array.from(bytes)));
63
+ return S.decodeUnknownSync(sha256DigestSchema)(`sha256:${Array.from(hash, (byte) => byte.toString(16).padStart(2, "0")).join("")}`);
64
+ }
65
+ /** Copies published content into a new draft; provenance is a claim, never authority. */
66
+ export function createWorkflowShare({ library, catalog, origin, }) {
67
+ async function save(workflowId, draft, provenance) {
68
+ const wanted = [
69
+ ...new Set(Object.values(draft.definition.apps).map((app) => app.appId)),
70
+ ];
71
+ const available = await catalog(wanted);
72
+ const appIds = wanted.filter((appId) => !Object.hasOwn(available, appId));
73
+ if (appIds.length)
74
+ return shareCopyReply.parse({ kind: "missing_apps", appIds });
75
+ const issues = validate(draft.definition, available);
76
+ if (issues.length)
77
+ return shareCopyReply.parse({ kind: "invalid", issues });
78
+ const result = await library.saveDraft(workflowId, { definition: draft.definition, layout: draft.layout, provenance }, 0);
79
+ return shareCopyReply.parse(result.kind === "saved"
80
+ ? {
81
+ ...result,
82
+ title: draft.definition.title,
83
+ review: review(draft.definition, available),
84
+ }
85
+ : result);
86
+ }
87
+ return {
88
+ async exportWorkflow(input) {
89
+ const document = await library.published(input.workflowId, {
90
+ version: input.version,
91
+ });
92
+ if (!document)
93
+ return { kind: "not_found" };
94
+ let source = { ...input, commit: document.commit };
95
+ if (origin !== undefined)
96
+ source = { ...source, origin };
97
+ const bundle = S.decodeUnknownSync(shareBundleSchema)({
98
+ format: "fungi.workflow-share/v1",
99
+ definition: document.definition,
100
+ layout: document.layout,
101
+ source,
102
+ }, { onExcessProperty: "error" });
103
+ const bytes = JSON.stringify(bundle);
104
+ const encoded = new TextEncoder().encode(bytes);
105
+ if (encoded.byteLength > maxShareBytes)
106
+ return { kind: "too_large" };
107
+ return { bytes, sha256: await digest(encoded) };
108
+ },
109
+ async importWorkflow(input) {
110
+ const bytes = new TextEncoder().encode(input.bytes);
111
+ if (bytes.byteLength > maxShareBytes)
112
+ return { kind: "too_large" };
113
+ if (!S.is(sha256DigestSchema)(input.sha256) ||
114
+ (await digest(bytes)) !== input.sha256)
115
+ return { kind: "digest_mismatch" };
116
+ const parsed = S.decodeUnknownResult(S.fromJsonString(shareBundleSchema))(input.bytes, { onExcessProperty: "error" });
117
+ if (Result.isFailure(parsed))
118
+ return { kind: "invalid_bundle" };
119
+ const bundle = parsed.success;
120
+ return save(input.workflowId, { definition: copyTitled(bundle.definition), layout: bundle.layout }, provenanceSchema.parse({
121
+ kind: "import",
122
+ ...bundle.source,
123
+ sha256: input.sha256,
124
+ }));
125
+ },
126
+ async forkPublishedWorkflow(input) {
127
+ const document = await library.published(input.sourceWorkflowId, input.at);
128
+ if (!document)
129
+ return { kind: "not_found" };
130
+ return save(input.workflowId, {
131
+ definition: copyTitled(document.definition),
132
+ layout: document.layout,
133
+ }, provenanceSchema.parse({
134
+ kind: "fork",
135
+ workflowId: input.sourceWorkflowId,
136
+ commit: document.commit,
137
+ }));
138
+ },
139
+ };
140
+ }
@@ -0,0 +1,98 @@
1
+ import * as z from "zod";
2
+ import { defineAppMethod } from "@fungi.computer/agar/backend-definition";
3
+ import { catalogReplySchema } from "@fungi.computer/workflows-wire";
4
+ import { runReply } from "@fungi.computer/workflows-wire";
5
+ import { workflowToolListing } from "../shared/tool-contracts.js";
6
+ const output = z.toJSONSchema(runReply, { io: "output" });
7
+ // Workflow ids have no underscores or upper-case letters. Preserve dash cases
8
+ // that cannot become camel case, so distinct ids cannot alias the same tool.
9
+ function toolName(workflowId) {
10
+ const camel = workflowId.replace(/-([a-z])|-/g, (_, letter) => letter === undefined ? "_" : letter.toUpperCase());
11
+ return /^[0-9]/.test(camel) ? `Workflow_${camel}` : camel;
12
+ }
13
+ /** A Workflow's own declared App roles: `{ alias: { appId, role? } }`. */
14
+ const appRoleSchema = z.record(z.string(), z.object({ appId: z.string(), role: z.string().optional() }));
15
+ /**
16
+ * Resolve each declared App role to exactly one installed App: the same
17
+ * unambiguous match the Run dialog's own default picks for a person. A role
18
+ * with zero or more than one installed candidate has no single correct
19
+ * answer without a person choosing, so the caller withholds the whole row
20
+ * rather than guessing a binding.
21
+ */
22
+ function resolveBindings(apps, installed) {
23
+ const bindings = {};
24
+ for (const [alias, spec] of Object.entries(apps)) {
25
+ const matches = installed.filter((app) => app.appId === spec.appId);
26
+ const [match] = matches;
27
+ if (matches.length !== 1 || match === undefined)
28
+ return null;
29
+ bindings[alias] = match.installationId;
30
+ }
31
+ return bindings;
32
+ }
33
+ async function listTools(indexes, workflows) {
34
+ const library = indexes?.libraryList();
35
+ if (!indexes || library?.kind !== "ready")
36
+ return { kind: "unindexed" };
37
+ // Read the Team's installed Apps once for every row, not once per row.
38
+ const catalog = workflows
39
+ ? catalogReplySchema.parse(await workflows.catalog({}))
40
+ : { error: "not_granted" };
41
+ const installed = "apps" in catalog
42
+ ? catalog.apps.map((app) => ({
43
+ installationId: app.installationId,
44
+ appId: app.appId,
45
+ }))
46
+ : [];
47
+ const tools = [];
48
+ for (const item of [...library.value.items].sort((a, b) => a.workflowId.localeCompare(b.workflowId))) {
49
+ const published = item.published;
50
+ if (published?.version == null)
51
+ continue;
52
+ // The immutable publication row written at publish; no Git is read here.
53
+ const row = indexes.publications.read(`${item.workflowId}:v${String(published.version)}`);
54
+ if (row.kind !== "ready" ||
55
+ row.value.workflowId !== item.workflowId ||
56
+ row.value.version !== published.version ||
57
+ row.value.commit !== published.commit)
58
+ return { kind: "unindexed" };
59
+ const definition = row.value.definition;
60
+ const apps = appRoleSchema.safeParse(definition.apps);
61
+ if (!apps.success)
62
+ continue;
63
+ const bindings = resolveBindings(apps.data, installed);
64
+ // No single correct binding today (a role is unmet or ambiguous): never
65
+ // guess one, so withhold this one Workflow's tool, not the whole family.
66
+ if (bindings === null)
67
+ continue;
68
+ tools.push({
69
+ workflowId: item.workflowId,
70
+ tool: toolName(item.workflowId),
71
+ title: definition.title,
72
+ description: definition.description ?? "",
73
+ version: published.version,
74
+ commit: published.commit,
75
+ input: definition.input,
76
+ output,
77
+ // The exact extra arguments a generic tool-family host merges with the
78
+ // caller's own `input` when it calls `runWorkflow`; see tool-contracts.
79
+ call: {
80
+ workflowId: item.workflowId,
81
+ version: published.version,
82
+ bindings,
83
+ },
84
+ });
85
+ }
86
+ return workflowToolListing.parse({ kind: "ready", tools });
87
+ }
88
+ export const toolFamilyMethods = {
89
+ listWorkflowTools: defineAppMethod({
90
+ description: "List typed tools from saved published Workflow versions; drafts are excluded.",
91
+ hidden: true,
92
+ effect: "read",
93
+ requires: ["workflow.run"],
94
+ input: z.strictObject({}),
95
+ output: workflowToolListing,
96
+ handler: (_input, context) => listTools(context.indexes, context.env.WORKFLOWS),
97
+ }),
98
+ };
@@ -0,0 +1,32 @@
1
+ import * as z from "zod";
2
+ import { gitSha1Schema } from "@fungi.computer/puffball/git-representation";
3
+ import { workflowIdSchema } from "@fungi.computer/workflows-wire";
4
+ /** Published inventory describes discovery, never grants authority to start a run. */
5
+ export const workflowToolListing = z.union([
6
+ z.strictObject({ kind: z.literal("unindexed") }),
7
+ z.strictObject({
8
+ kind: z.literal("ready"),
9
+ tools: z
10
+ .array(z.strictObject({
11
+ workflowId: workflowIdSchema,
12
+ tool: z
13
+ .string()
14
+ .min(1)
15
+ .max(80)
16
+ .regex(/^[A-Za-z][A-Za-z0-9_]*$/),
17
+ title: z.string().max(120),
18
+ description: z.string().max(1000),
19
+ version: z.number().int().positive(),
20
+ commit: gitSha1Schema,
21
+ input: z.record(z.string(), z.json()),
22
+ output: z.record(z.string(), z.json()),
23
+ // Opaque to every generic tool-family host: the exact extra
24
+ // arguments this App wants merged with the caller's `input` when
25
+ // it dispatches to `runWorkflow` (its pinned workflowId, version
26
+ // and the App bindings resolved for this published version). A
27
+ // host spreads this verbatim; it never invents or defaults it.
28
+ call: z.record(z.string(), z.json()),
29
+ }))
30
+ .max(100),
31
+ }),
32
+ ]);
package/package.json CHANGED
@@ -1,6 +1,169 @@
1
1
  {
2
2
  "name": "@fungi.computer/trellis",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
3
+ "description": "Trellis: build and run workflows of App methods, an App for Oyster",
4
+ "version": "0.0.1",
5
+ "author": "Levi <levi@technomancy.dev>",
6
+ "type": "module",
7
+ "exports": {
8
+ "./backend": "./backend/index.ts",
9
+ "./library": {
10
+ "types": "./backend/library.ts",
11
+ "import": "./dist/backend/library.js"
12
+ }
13
+ },
14
+ "dependencies": {
15
+ "@fungi.computer/puffball": "0.0.2",
16
+ "@cfworker/json-schema": "4.1.1",
17
+ "@fungi.computer/agar": "0.1.0",
18
+ "@fungi.computer/caps": "0.0.2",
19
+ "@fungi.computer/stipe": "0.0.1",
20
+ "@xyflow/react": "12.12.0",
21
+ "react": "18.3.1",
22
+ "react-dom": "18.3.1",
23
+ "@fungi.computer/braid": "0.0.1",
24
+ "effect": "4.0.2",
25
+ "@fungi.computer/sigils": "0.0.1",
26
+ "lucide-react": "0.460.0",
27
+ "zod": "4.4.3",
28
+ "@fungi.computer/workflows-wire": "0.0.1",
29
+ "@fungi.computer/cairn": "0.0.1",
30
+ "@fungi.computer/watchdog": "0.0.1",
31
+ "@fungi.computer/maosaico": "0.0.1"
32
+ },
33
+ "devDependencies": {
34
+ "@cloudflare/workers-types": "5.20260828.1",
35
+ "@ladle/react": "5.1.1",
36
+ "@tailwindcss/vite": "4.2.1",
37
+ "@types/react": "18.3.3",
38
+ "@types/react-dom": "18.3.0",
39
+ "@vitejs/plugin-react": "6.1.1",
40
+ "playwright": "1.62.1",
41
+ "typescript": "5.9.3",
42
+ "vite": "8.2.2",
43
+ "vite-plugin-singlefile": "2.3.3",
44
+ "vitest": "4.1.11",
45
+ "eslint": "10.9.1",
46
+ "tsx": "4.23.12",
47
+ "jsdom": "29.0.1",
48
+ "@types/node": "24.13.3"
49
+ },
50
+ "license": "MIT",
51
+ "repository": {
52
+ "type": "git",
53
+ "url": "git+https://github.com/fungi-computer/trellis.git"
54
+ },
55
+ "botanical": {
56
+ "role": "app",
57
+ "operations": {
58
+ "lint": "required",
59
+ "typecheck": "required",
60
+ "build": "required",
61
+ "test": "required",
62
+ "docs": {
63
+ "status": "not-applicable",
64
+ "reason": "Private first-party App"
65
+ },
66
+ "pack": {
67
+ "status": "not-applicable",
68
+ "reason": "Delivered as captured HTML"
69
+ },
70
+ "smoke": {
71
+ "status": "not-applicable",
72
+ "reason": "Browser proof exercises the App"
73
+ },
74
+ "stranger": {
75
+ "status": "not-applicable",
76
+ "reason": "Private first-party App"
77
+ }
78
+ }
79
+ },
80
+ "fungi": {
81
+ "backendDefinition": "backend/methods.ts",
82
+ "app": {
83
+ "displayName": "Trellis",
84
+ "icon": "run",
85
+ "capabilities": [
86
+ "computer.list",
87
+ "computer.readFile",
88
+ "computer.writeFile",
89
+ "media.generation",
90
+ "workflow.run",
91
+ "workflow.publish",
92
+ "workflow.arm",
93
+ "git.repository",
94
+ "agent.read"
95
+ ],
96
+ "handlers": [],
97
+ "roles": [],
98
+ "backend": {
99
+ "bindings": [
100
+ {
101
+ "name": "GIT",
102
+ "operations": [
103
+ "read",
104
+ "write",
105
+ "create",
106
+ "exchange"
107
+ ]
108
+ },
109
+ {
110
+ "name": "WORKFLOWS",
111
+ "operations": [
112
+ "catalog",
113
+ "runGraph",
114
+ "graphStatus",
115
+ "cancel",
116
+ "answerReview",
117
+ "armSchedule",
118
+ "disarmSchedule",
119
+ "schedules",
120
+ "armEventTrigger",
121
+ "disarmEventTrigger",
122
+ "eventTriggers",
123
+ "runs"
124
+ ]
125
+ },
126
+ {
127
+ "name": "INBOX",
128
+ "operations": [
129
+ "page"
130
+ ]
131
+ }
132
+ ]
133
+ }
134
+ },
135
+ "build": {
136
+ "react": true,
137
+ "stylesheets": [
138
+ "src/trellis.css"
139
+ ]
140
+ }
141
+ },
142
+ "files": [
143
+ "dist/backend",
144
+ "dist/shared",
145
+ "backend",
146
+ "shared",
147
+ "README.md",
148
+ "LICENSE"
149
+ ],
150
+ "scripts": {
151
+ "build": "vite build && tsc -p backend/tsconfig.build.json",
152
+ "typecheck": "tsc -p tsconfig.json --noEmit && tsc -p backend/tsconfig.json --noEmit",
153
+ "lint": "eslint src test backend --max-warnings=0",
154
+ "rebuild-copies": "node --import tsx scripts/rebuild-copies.mjs",
155
+ "test": "node --import tsx --test test/*.test.ts test/storyboard-method-pins.test.mjs && vitest run --config vitest.config.mjs test/main.test.tsx test/inspector.test.tsx test/use-draft.test.tsx test/review-card.test.tsx test/runs.test.tsx test/version-history.test.tsx test/portable-share.test.tsx test/review-output.test.tsx test/publish-controls.test.tsx",
156
+ "test:readiness:browser": "node test/editor-w4.browser.mjs && node test/readiness.browser.mjs",
157
+ "test:browser": "node test/workflows.browser.mjs",
158
+ "test:storyboard:browser": "node test/storyboard.browser.mjs",
159
+ "format:check": "node ../../tooling/repository/scripts/format-package-check.mjs",
160
+ "lint:anti-slop": "node ../../tooling/repository/scripts/lint-package-anti-slop.mjs",
161
+ "test:nightly": "WORKFLOW_EVIDENCE=.botanical/workflows bash ../../.agents/skills/orchestrate-multi-lane-work/scripts/run-proof.sh node test/workflows.browser.mjs && pnpm run test:history:browser",
162
+ "test:editor:browser": "node test/editor.browser.mjs && node test/editor-w4.browser.mjs",
163
+ "test:generic:browser": "node test/workflows-generic.browser.mjs",
164
+ "test:nested:browser": "node test/nested-fields.browser.mjs",
165
+ "test:graph:browser": "node test/graph-story.browser.mjs",
166
+ "test:history:browser": "node --import tsx test/history.browser.mjs",
167
+ "test:layout:browser": "node test/layout.browser.mjs"
168
+ }
6
169
  }
@@ -0,0 +1,35 @@
1
+ import * as z from "zod";
2
+ import { gitSha1Schema } from "@fungi.computer/puffball/git-representation";
3
+ import { workflowIdSchema } from "@fungi.computer/workflows-wire";
4
+
5
+ /** Published inventory describes discovery, never grants authority to start a run. */
6
+ export const workflowToolListing = z.union([
7
+ z.strictObject({ kind: z.literal("unindexed") }),
8
+ z.strictObject({
9
+ kind: z.literal("ready"),
10
+ tools: z
11
+ .array(
12
+ z.strictObject({
13
+ workflowId: workflowIdSchema,
14
+ tool: z
15
+ .string()
16
+ .min(1)
17
+ .max(80)
18
+ .regex(/^[A-Za-z][A-Za-z0-9_]*$/),
19
+ title: z.string().max(120),
20
+ description: z.string().max(1000),
21
+ version: z.number().int().positive(),
22
+ commit: gitSha1Schema,
23
+ input: z.record(z.string(), z.json()),
24
+ output: z.record(z.string(), z.json()),
25
+ // Opaque to every generic tool-family host: the exact extra
26
+ // arguments this App wants merged with the caller's `input` when
27
+ // it dispatches to `runWorkflow` (its pinned workflowId, version
28
+ // and the App bindings resolved for this published version). A
29
+ // host spreads this verbatim; it never invents or defaults it.
30
+ call: z.record(z.string(), z.json()),
31
+ }),
32
+ )
33
+ .max(100),
34
+ }),
35
+ ]);