toolroll 0.9.40 → 0.9.42
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/CHANGELOG.md +28 -0
- package/dist/cli.js +3 -1
- package/dist/coding-types.d.ts +2 -24
- package/dist/coding-workspace.js +12 -11
- package/dist/contracts/cli.d.ts +736 -0
- package/dist/contracts/cli.js +223 -0
- package/dist/contracts/coding-activity.d.ts +64 -0
- package/dist/contracts/coding-activity.js +58 -0
- package/dist/contracts/console-api.d.ts +483 -0
- package/dist/contracts/console-api.js +273 -0
- package/dist/contracts/diff-stat.d.ts +58 -0
- package/dist/contracts/diff-stat.js +59 -0
- package/dist/contracts/integration-metadata.d.ts +99 -0
- package/dist/contracts/integration-metadata.js +101 -0
- package/dist/contracts/model-catalog.d.ts +21 -0
- package/dist/contracts/model-catalog.js +14 -0
- package/dist/contracts/native-census.d.ts +42 -0
- package/dist/contracts/native-census.js +36 -0
- package/dist/contracts/observation-request.d.ts +37 -0
- package/dist/contracts/observation-request.js +25 -0
- package/dist/contracts/provider.d.ts +30 -0
- package/dist/contracts/provider.js +15 -0
- package/dist/contracts/recipes.d.ts +166 -0
- package/dist/contracts/recipes.js +52 -0
- package/dist/contracts/retention.d.ts +42 -0
- package/dist/contracts/retention.js +24 -0
- package/dist/contracts/storage.d.ts +23 -0
- package/dist/contracts/storage.js +10 -0
- package/dist/contracts/store-json.d.ts +315 -0
- package/dist/contracts/store-json.js +196 -0
- package/dist/contracts/trigger-payloads.d.ts +369 -0
- package/dist/contracts/trigger-payloads.js +144 -0
- package/dist/contracts/update-journal.d.ts +254 -0
- package/dist/contracts/update-journal.js +155 -0
- package/dist/contracts/verification-view.d.ts +93 -0
- package/dist/contracts/verification-view.js +43 -0
- package/dist/control-setup.d.ts +1 -1
- package/dist/desktop-update.d.ts +7 -40
- package/dist/desktop-update.js +36 -8
- package/dist/evidence.d.ts +3 -19
- package/dist/flow-triggers.js +61 -60
- package/dist/mcp-connect.d.ts +0 -1
- package/dist/mcp-connect.js +41 -23
- package/dist/model-catalog.js +8 -6
- package/dist/observations.d.ts +2 -6
- package/dist/observations.js +13 -9
- package/dist/operate.js +2 -1
- package/dist/process-recovery-native.d.ts +2 -13
- package/dist/process-recovery-native.js +8 -5
- package/dist/project-tools.d.ts +6 -24
- package/dist/project-tools.js +22 -18
- package/dist/provider.d.ts +3 -8
- package/dist/provider.js +15 -11
- package/dist/recipes.d.ts +3 -28
- package/dist/recipes.js +79 -52
- package/dist/retention.d.ts +4 -5
- package/dist/retention.js +5 -4
- package/dist/serve.js +142 -28
- package/dist/storage.d.ts +2 -1
- package/dist/storage.js +12 -9
- package/dist/store.d.ts +3 -15
- package/dist/store.js +80 -158
- package/dist/surface.d.ts +6 -54
- package/dist/surface.js +6 -1
- package/dist/toolroll-update.d.ts +8 -99
- package/dist/toolroll-update.js +28 -18
- package/dist/verification-evidence.js +27 -12
- package/package.json +1 -1
|
@@ -0,0 +1,273 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The console's form bodies and JSON API (docs/plans/zod-revamp.md, item 13): one schema for every URL-encoded body a
|
|
3
|
+
* console route reads, and one for each `?format=json` response.
|
|
4
|
+
*
|
|
5
|
+
* A form is read exactly as before. Each field is what `URLSearchParams` holds for it — every value in the order
|
|
6
|
+
* sent, so `get()` is the first and `getAll()` the list — and every field may be left out, because each handler
|
|
7
|
+
* already supplies its own default, trimming, clipping, number conversion and refusal after reading. Unknown fields
|
|
8
|
+
* are ignored (browsers send `csrf`, buttons send their own names, and older pages send fields no longer read). What a
|
|
9
|
+
* handler may read is the schema's field list: `readForm` hands it a view typed by that list, so reading a field the
|
|
10
|
+
* contract doesn't name fails the typecheck instead of drifting. Nothing a person or script sent before is refused
|
|
11
|
+
* now; the CSRF, nonce, digest, password and duplicate-field checks stay in the handlers, in their order.
|
|
12
|
+
*
|
|
13
|
+
* The JSON responses are checked as they are sent (unversioned, unknown keys allowed, their bytes unchanged — an
|
|
14
|
+
* evidence pack carries sealed ledger entries); a response that disagrees is logged and sent anyway.
|
|
15
|
+
*
|
|
16
|
+
* Not here: Telegram's pushed updates (item 11), flow webhooks and public flow forms (item 15).
|
|
17
|
+
*/
|
|
18
|
+
import { z } from "zod";
|
|
19
|
+
import { parseContract } from "./contract.js";
|
|
20
|
+
/** One form field: every value sent for it, in order. Text only — a URL-encoded body has nothing else. */
|
|
21
|
+
const formField = z.array(z.string());
|
|
22
|
+
/** A form's contract: a loose object of optional text fields (unknown fields ignored, as the handlers always did). */
|
|
23
|
+
export function formContract(route, fields, more = {}) {
|
|
24
|
+
const schema = z.looseObject(Object.fromEntries(fields.map(field => [field, formField.optional()]))).catchall(formField);
|
|
25
|
+
return { route, fields, prefixes: more.prefixes ?? [], dynamic: more.dynamic ?? false, schema: schema };
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* A body as its schema sees it: each field name once, in first-sent order, with all its values in sent order. Built
|
|
29
|
+
* with `Object.fromEntries`, so a field named `__proto__` is an ordinary field.
|
|
30
|
+
*/
|
|
31
|
+
export function formFields(body) {
|
|
32
|
+
const fields = new Map();
|
|
33
|
+
for (const [name, value] of body) {
|
|
34
|
+
const values = fields.get(name);
|
|
35
|
+
if (values === undefined)
|
|
36
|
+
fields.set(name, [value]);
|
|
37
|
+
else
|
|
38
|
+
values.push(value);
|
|
39
|
+
}
|
|
40
|
+
return Object.fromEntries(fields);
|
|
41
|
+
}
|
|
42
|
+
const toStderr = (what, issues) => {
|
|
43
|
+
process.stderr.write(`${what}: disagrees with its contract — ${issues.map(one => one.line).join("; ")}\n`);
|
|
44
|
+
};
|
|
45
|
+
/**
|
|
46
|
+
* Read a form through its contract. It never refuses: every value is text and every field optional, so a body that
|
|
47
|
+
* somehow disagrees is reported and read as it was sent (the handler's own checks decide, as before).
|
|
48
|
+
*/
|
|
49
|
+
export function readForm(body, contract, report = toStderr) {
|
|
50
|
+
const parsed = parseContract(contract.schema, formFields(body));
|
|
51
|
+
if (!parsed.ok)
|
|
52
|
+
report(`form ${contract.route}`, parsed.issues);
|
|
53
|
+
return {
|
|
54
|
+
get: name => body.get(name),
|
|
55
|
+
getAll: name => body.getAll(name),
|
|
56
|
+
has: name => body.has(name),
|
|
57
|
+
keys: () => body.keys(),
|
|
58
|
+
sent: body,
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
// ---- every console form ------------------------------------------------------------------------------------------
|
|
62
|
+
//
|
|
63
|
+
// One per route (or per verb, where one route takes several bodies). A family whose sub-routes share one dispatch —
|
|
64
|
+
// `/code/*`, `/flows/*`, the teammate pages, Slack/Teams/Discord — has one contract naming every field its handlers
|
|
65
|
+
// read. Every body also passes through `mutationGuardForm` (the shared CSRF and duplicate check, unchanged) and
|
|
66
|
+
// `ledgerTargetForm` (the project a request is recorded against).
|
|
67
|
+
export const CONSOLE_FORMS = {
|
|
68
|
+
// Before sign-in.
|
|
69
|
+
signup: formContract("POST /signup", ["code", "name", "password"]),
|
|
70
|
+
login: formContract("POST /login", ["name", "token", "return"]),
|
|
71
|
+
join: formContract("POST /join/<invite>", ["name", "password"]),
|
|
72
|
+
// Shared by every signed-in POST.
|
|
73
|
+
mutationGuard: formContract("POST (every signed-in form)", ["csrf", "token", "digest", "nonce", "confirm"]),
|
|
74
|
+
ledgerTarget: formContract("POST (the action ledger's project)", ["repo"]),
|
|
75
|
+
projectCeiling: formContract("POST (project ceiling)", ["repo"]),
|
|
76
|
+
demo: formContract("POST /chat/demo/<act>", ["message", "note"]),
|
|
77
|
+
updatesDismiss: formContract("POST /settings/updates/dismiss", ["version", "quiet", "return"]),
|
|
78
|
+
updatesChecks: formContract("POST /settings/updates/checks", ["check"]),
|
|
79
|
+
code: formContract("POST /code, /code/start, /code/<session>/<act>", ["repo", "password", "title", "model", "prompt", "requestId", "base", "candidate", "goal", "acceptance", "visual", "decision", "answers"], { prefixes: ["question:"] }),
|
|
80
|
+
skillsRevise: formContract("POST /settings/skills/revise", ["run", "repo", "feedback", "nonce"]),
|
|
81
|
+
skillsChange: formContract("POST /settings/skills/import, /settings/skills/change", ["repo", "identity", "revision", "method", "content", "files", "url", "action", "sha", "sample", "nonce", "restore"]),
|
|
82
|
+
kit: formContract("POST /kits/<kit>/<act>", ["repo"]),
|
|
83
|
+
teammates: formContract("POST /teammates/new, /teammates/<id>/<part>, /teammates/questions/<id>/answer", ["choice", "text", "repo", "template", "name", "soul", "op", "id", "schedule", "tool", "state", "note", "model", "dailyTurns", "manager"], { prefixes: ["use.", "undo.", "field.", "over."] }),
|
|
84
|
+
galleryUse: formContract("POST /flows/new/<template>", ["repo", "send-result", "name", "intent", "previewed"], { dynamic: true }),
|
|
85
|
+
flows: formContract("POST /flows/new, /flows/example, /flows/import, /flows/<id>/<act>, /flows/<id>/cards/<card>/<act>, /flows/<id>/triggers/<trigger>/<act>", ["repo", "document", "url", "confirm", "previewed", "template", "name", "answers", "password", "secret", "trigger", "approve", "remove", "about", "body", "timeoutMinutes", "language", "file", "address", "key", "definition", "owner", "revision", "value", "title", "description", "stage", "decision", "note", "draft", "choice", "entry", "label", "watching"], { prefixes: ["param."] }),
|
|
86
|
+
spendBudget: formContract("POST /spend/budget", ["password", "target", "action", "usd", "stop"]),
|
|
87
|
+
projectConcurrency: formContract("POST /settings/project/concurrency", ["repo", "concurrency"]),
|
|
88
|
+
projectDelete: formContract("POST /settings/project/delete", ["repo", "name", "step", "password"]),
|
|
89
|
+
policy: formContract("POST /settings/policy", ["password", "provider", "models", "tools", "ceiling"]),
|
|
90
|
+
approval: formContract("POST /settings/approval", ["repo", "password", "protect", "paths", "not_requester"]),
|
|
91
|
+
sessions: formContract("POST /settings/sessions", ["everyone", "action", "session", "token", "name", "access", "days", "password"]),
|
|
92
|
+
signIn: formContract("POST /settings/sign-in", ["action", "password", "issuer", "client-id", "client-secret", "label", "scopes", "groups-claim", "passwords"], { prefixes: ["group-", "role-", "projects-"] }),
|
|
93
|
+
updates: formContract("POST /settings/updates, /settings/updates/seen, /settings/updates/cancel", ["password", "version", "when"]),
|
|
94
|
+
retention: formContract("POST /settings/retention", ["password", "evidence", "checkouts", "chat", "notifications"]),
|
|
95
|
+
storage: formContract("POST /settings/storage, /settings/storage/clean, /settings/storage/discard", ["password", "cleanup", "preview", "path"]),
|
|
96
|
+
pullRequests: formContract("POST /settings/pull-requests", ["repo", "act", "password", "github", "base", "method", "when-green"]),
|
|
97
|
+
checks: formContract("POST /settings/checks", ["repo", "password", "act", "on", "level", "command", "timeout"]),
|
|
98
|
+
backups: formContract("POST /settings/backups", ["password", "every", "keep", "folder"]),
|
|
99
|
+
data: formContract("POST /settings/data", ["password"]),
|
|
100
|
+
flowsOn: formContract("POST /settings/flows/on", ["repo", "starter"]),
|
|
101
|
+
integrationsTest: formContract("POST /settings/integrations/test", ["key"]),
|
|
102
|
+
monitoring: formContract("POST /settings/monitoring", ["password", "webhook", "folder", "traces", "header-name", "header-value", "rotate"]),
|
|
103
|
+
toolsConnect: formContract("POST /settings/tools/connect", ["repo", "also", "service", "template", "kit", "shown", "password", "access"]),
|
|
104
|
+
toolsChange: formContract("POST /settings/tools/change", ["repo", "shown", "action", "name", "password", "catalog", "transport", "target", "secrets", "import", "secret", "value"]),
|
|
105
|
+
appearance: formContract("POST /settings/appearance", ["accent", "quiet", "return", "theme"]),
|
|
106
|
+
models: formContract("POST /settings/models/check, /settings/models/watch, /settings/models/update, /settings/models/agent", ["enabled", "tool", "phase", "agent"]),
|
|
107
|
+
knowledgeRefresh: formContract("POST /settings/knowledge/refresh", ["repo"]),
|
|
108
|
+
knowledgeProposal: formContract("POST /settings/knowledge/proposal", ["repo", "decision", "proposal"]),
|
|
109
|
+
knowledgeDecision: formContract("POST /settings/knowledge/decision", ["repo", "action", "claim", "why", "decision", "reason"]),
|
|
110
|
+
knowledgeChange: formContract("POST /settings/knowledge/change", ["repo", "action", "identity", "revision", "instructions", "title", "content", "path", "id", "restore"]),
|
|
111
|
+
learningChange: formContract("POST /settings/learning/change", ["repo", "action", "identity", "revision", "lesson", "version", "sha"]),
|
|
112
|
+
projectSetup: formContract("POST /control/setup-preview, /control/setup-approve, /control/instructions-preview, /control/instructions-approve", ["repo", "nonce", "fingerprint", "token", "provider", "model", "command", "seconds"]),
|
|
113
|
+
slack: formContract("POST /settings/slack/<connect|pair|unpair|disconnect|alerts>", ["password", "app-token", "bot-token"]),
|
|
114
|
+
telegramPair: formContract("POST /settings/telegram/pair, /settings/telegram/unpair", ["password"]),
|
|
115
|
+
teams: formContract("POST /settings/teams/<connect|pair|unpair|disconnect|alerts>", ["password", "app-id", "tenant", "secret"]),
|
|
116
|
+
discord: formContract("POST /settings/discord/<connect|pair|unpair|disconnect|alerts>", ["password", "bot-token"]),
|
|
117
|
+
messaging: formContract("POST /settings/messaging", ["primary"]),
|
|
118
|
+
permissionDefault: formContract("POST /settings/permission-default", ["permission-mode"]),
|
|
119
|
+
qualityDefault: formContract("POST /settings/quality-default", ["quality-mode"]),
|
|
120
|
+
notifications: formContract("POST /settings/notifications", ["mode", "digest", "screenshots"]),
|
|
121
|
+
notificationsMute: formContract("POST /settings/notifications/mute", ["repo", "pings"]),
|
|
122
|
+
telegramDigest: formContract("POST /settings/telegram-digest", ["every"]),
|
|
123
|
+
providerKey: formContract("POST /settings/provider-key, /settings/provider-key-clear", ["provider", "auth-mode", "value"]),
|
|
124
|
+
telegramToken: formContract("POST /settings/telegram-token", ["token"]),
|
|
125
|
+
email: formContract("POST /settings/email, /settings/email-test, /settings/email-read-test", ["host", "port", "secure", "user", "from", "password", "imapHost", "imapPort"]),
|
|
126
|
+
google: formContract("POST /settings/google, /settings/google/disconnect", ["clientId", "clientSecret"]),
|
|
127
|
+
projectsSelect: formContract("POST /projects/select", ["path", "return"]),
|
|
128
|
+
projectsRemove: formContract("POST /projects/remove", ["repo"]),
|
|
129
|
+
projectsOpen: formContract("POST /projects/open", ["path", "return"]),
|
|
130
|
+
tasksAdd: formContract("POST /tasks/add", ["projectRevision", "id", "title", "repo", "goal", "not", "touches", "scout", "permission-mode", "quality-mode", "after", "acceptance", "planning-policy", "plan-first"]),
|
|
131
|
+
queue: formContract("POST /queue/move, /queue/note", ["runner", "note", "from", "projectRevision", "respond", "task", "column", "before", "queueRevision"]),
|
|
132
|
+
runnerRegister: formContract("POST /fleet/runner/register", ["token", "name", "capacity"]),
|
|
133
|
+
mode: formContract("POST /mode/confirm, /mode/sign", ["name", "days", "auto-approve", "plan-auto", "allow-paid-fallback", "chat-approve", "publication", "expiry", "nonce", "digest", "token", "review-auto", "review-retry-auto", "repair-auto"]),
|
|
134
|
+
peopleProjects: formContract("POST /people/projects", ["token", "name", "access", "projects"]),
|
|
135
|
+
peopleInvite: formContract("POST /people/invite", ["token", "role", "access", "projects"]),
|
|
136
|
+
peopleInviteRevoke: formContract("POST /people/invite-revoke", ["token", "id"]),
|
|
137
|
+
peopleRevoke: formContract("POST /people/revoke", ["token", "name"]),
|
|
138
|
+
runnerRetire: formContract("POST /fleet/runner/retire", ["token", "name"]),
|
|
139
|
+
decisionAnswer: formContract("POST /d/<id>/answer", ["return", "choice", "confirm", "note"]),
|
|
140
|
+
contest: formContract("POST /contest/<id>/arm|pick|abandon", ["act", "choice", "token", "nonce"]),
|
|
141
|
+
attend: formContract("POST /t/<task>/attend|attend-preview|attend-revoke", ["parent", "followup", "minutes", "turns", "budget", "expiry", "model", "posture", "nonce", "digest", "token"]),
|
|
142
|
+
confirmStopped: formContract("POST /t/<task>/confirm-stopped", ["run", "token", "checked", "return"]),
|
|
143
|
+
merge: formContract("POST /t/<task>/merge", ["run", "token", "anyway"]),
|
|
144
|
+
onboard: formContract("POST /projects/onboard-preview, /projects/onboard-confirm", ["repo", "root", "token", "nonce", "big-ok"]),
|
|
145
|
+
pushSubscribe: formContract("POST /push/subscribe", ["token", "endpoint", "p256dh", "auth"]),
|
|
146
|
+
pushRemove: formContract("POST /push/remove", ["id"]),
|
|
147
|
+
phoneDismiss: formContract("POST /onboarding/phone/dismiss", ["quiet"]),
|
|
148
|
+
leadIdentity: formContract("POST /settings/lead/identity", ["name", "persona"]),
|
|
149
|
+
leadAbout: formContract("POST /settings/lead/about", ["about"]),
|
|
150
|
+
leadPromiseCancel: formContract("POST /settings/lead/promise/cancel", ["promise"]),
|
|
151
|
+
chatConfig: formContract("POST /chat/config", ["return", "token", "off", "forget-key", "provider", "model", "weekly-usd", "daily-turns", "key"]),
|
|
152
|
+
mateMint: formContract("POST /chat/mate/mint", ["return", "ceiling-usd", "token", "follow"]),
|
|
153
|
+
mateFollow: formContract("POST /chat/mate/follow", ["enabled", "return"]),
|
|
154
|
+
mateEnd: formContract("POST /chat/mate/end", ["return"]),
|
|
155
|
+
mateStop: formContract("POST /chat/mate/stop", ["return", "turn"]),
|
|
156
|
+
mateProposal: formContract("POST /chat/proposal/<id>/confirm|dismiss", ["return", "nonce", "confirm", "token"]),
|
|
157
|
+
coordinatorProposal: formContract("POST /proposals/<id>/confirm|dismiss", ["return", "confirm"]),
|
|
158
|
+
chat: formContract("POST /chat", ["task", "project", "result", "mode", "request", "request-session", "message", "token"]),
|
|
159
|
+
chatFile: formContract("POST /chat/file/<id>", ["token"]),
|
|
160
|
+
chatAck: formContract("POST /chat/ack/<id>", ["token", "nonce"]),
|
|
161
|
+
// A recipe's own answers and editor fields are read by recipes.ts from the whole form (`sent`; item 17).
|
|
162
|
+
recipes: formContract("POST /recipes/prepare, /recipes/preview, /recipes/import, /recipes/launch, /recipes/save", ["repo", "projectRevision", "recipe", "recipeRevision", "document", "purpose", "source", "sourceRevision", "preview", "next"]),
|
|
163
|
+
routinesAdd: formContract("POST /routines/add", ["projectRevision", "name", "ceiling", "repeat", "interval", "interval-unit", "time", "timezone", "weekday", "schedule", "goal", "not", "touches", "acceptance"]),
|
|
164
|
+
routine: formContract("POST /routines/<id>/approve|refresh|pause|resume|run-now", ["digest", "nonce", "token"]),
|
|
165
|
+
runNote: formContract("POST /r/<run>/note", ["note"]),
|
|
166
|
+
diffComment: formContract("POST /r/<run>/comment", ["return", "tab", "intent", "batch", "source", "note", "path", "line", "request"]),
|
|
167
|
+
editorLinks: formContract("POST /session/editor-links", ["on", "return"]),
|
|
168
|
+
turn: formContract("POST /r/<run>/turn", ["text"]),
|
|
169
|
+
revise: formContract("POST /r/<run>/revise", ["return", "batch", "source"]),
|
|
170
|
+
followUp: formContract("POST /r/<run>/checks|add-tests", ["return", "level"]),
|
|
171
|
+
resolveIncident: formContract("POST /i/<id>/resolve", ["return"]),
|
|
172
|
+
// A task's verbs (/t/<task>/<verb>), one body each.
|
|
173
|
+
taskSteer: formContract("POST /t/<task>/steer", ["note"]),
|
|
174
|
+
taskHold: formContract("POST /t/<task>/hold", ["reason"]),
|
|
175
|
+
taskBlock: formContract("POST /t/<task>/block|unblock", ["on"]),
|
|
176
|
+
taskRepairDependency: formContract("POST /t/<task>/repair-dependency", ["blocker", "operation", "replacement"]),
|
|
177
|
+
taskNext: formContract("POST /t/<task>/next", ["undo"]),
|
|
178
|
+
taskReopen: formContract("POST /t/<task>/reopen", ["token"]),
|
|
179
|
+
taskRequeue: formContract("POST /t/<task>/requeue", ["note", "return"]),
|
|
180
|
+
taskFollowUp: formContract("POST /t/<task>/follow-up", ["index"]),
|
|
181
|
+
taskPlanEdit: formContract("POST /t/<task>/plan-edit", ["saw-plan", "plan-document"]),
|
|
182
|
+
taskRevision: formContract("POST /t/<task>/accept-revision|reject-revision", ["revision-id", "token"]),
|
|
183
|
+
taskCancel: formContract("POST /t/<task>/cancel", ["reason"]),
|
|
184
|
+
taskRoute: formContract("POST /t/<task>/route", ["risk", "size", "risky", "phase", "clear-phase", "agent", "provider", "model", "sawDigest", "return"]),
|
|
185
|
+
taskScope: formContract("POST /t/<task>/scope", ["sawDigest", "permission-mode", "quality-mode", "budget-usd", "budget-microusd", "requirement", "requirement-new", "race-count", "race-model", "race-per-usd", "race-total-usd", "goal-brief", "goal", "not", "touches", "acceptance"], { prefixes: ["compare-provider-", "compare-model-"] }),
|
|
186
|
+
taskApprove: formContract("POST /t/<task>/approve", ["return", "digest", "token", "nonce"]),
|
|
187
|
+
taskAcceptProof: formContract("POST /t/<task>/accept-proof", ["run", "note", "return"]),
|
|
188
|
+
taskComplete: formContract("POST /t/<task>/complete", ["receipt", "run", "accept", "note", "publish", "return"]),
|
|
189
|
+
taskStop: formContract("POST /t/<task>/stop", ["run", "return"]),
|
|
190
|
+
taskResume: formContract("POST /t/<task>/resume|resume-arm", ["run", "token", "nonce", "return"]),
|
|
191
|
+
};
|
|
192
|
+
/** The console POSTs whose handlers read no field (sign-out and the attended beat not even the shared guard): their contract is the empty form. */
|
|
193
|
+
export const BODILESS_POSTS = [
|
|
194
|
+
"POST /settings/lead/on",
|
|
195
|
+
"POST /r/<run>/draft-repair",
|
|
196
|
+
"POST /ledger/checkpoint",
|
|
197
|
+
"POST /settings/backups/now",
|
|
198
|
+
"POST /mode/revoke",
|
|
199
|
+
"POST /settings/telegram/retry",
|
|
200
|
+
"POST /providers/<provider>/resume",
|
|
201
|
+
"POST /session/attended-beats",
|
|
202
|
+
"POST /logout",
|
|
203
|
+
"POST /t/<task>/unhold",
|
|
204
|
+
"POST /t/<task>/plan",
|
|
205
|
+
"POST /t/<task>/retry-review",
|
|
206
|
+
];
|
|
207
|
+
// ---- the JSON API ------------------------------------------------------------------------------------------------
|
|
208
|
+
const ledgerEntrySchema = z.looseObject({
|
|
209
|
+
id: z.number(), at: z.string(), actor: z.string(), repo: z.string().nullable(), taskId: z.string().nullable(), runId: z.number().nullable(),
|
|
210
|
+
action: z.string(), outcome: z.string(), source: z.string(), detail: z.string().nullable(),
|
|
211
|
+
});
|
|
212
|
+
/** `GET /ledger?format=json`: the chain's state, a page of entries (newest first) and the cursor for the next. */
|
|
213
|
+
export const ledgerPageSchema = z.looseObject({
|
|
214
|
+
chain: z.looseObject({ ok: z.boolean(), entries: z.number(), through: z.number().nullable(), head: z.string(), problem: z.looseObject({ id: z.number().nullable(), what: z.string() }).nullable() }),
|
|
215
|
+
entries: z.array(ledgerEntrySchema),
|
|
216
|
+
nextBefore: z.number().nullable(),
|
|
217
|
+
});
|
|
218
|
+
/** `GET /t/<task>/evidence?format=json`: a task's evidence pack, as `evidence-pack.ts` builds and digests it. */
|
|
219
|
+
export const evidencePackSchema = z.looseObject({
|
|
220
|
+
format: z.string(),
|
|
221
|
+
generatedAt: z.string(),
|
|
222
|
+
generatedBy: z.string(),
|
|
223
|
+
task: z.looseObject({ id: z.string(), title: z.string(), state: z.string(), repo: z.string().nullable(), project: z.string().nullable(), filedAt: z.string() }),
|
|
224
|
+
rules: z.looseObject({ now: z.string(), changes: z.array(z.looseObject({ id: z.number(), at: z.string(), by: z.string(), change: z.string().nullable() })) }).nullable(),
|
|
225
|
+
versions: z.array(z.looseObject({ id: z.string(), title: z.string(), state: z.string(), filedAt: z.string(), scope: z.looseObject({ digest: z.string() }).nullable(), runs: z.array(z.looseObject({ id: z.number() })) })),
|
|
226
|
+
result: z.looseObject({ runId: z.number() }).nullable(),
|
|
227
|
+
totals: z.looseObject({ runs: z.number(), costUsd: z.number(), tokensIn: z.number(), tokensOut: z.number() }),
|
|
228
|
+
ledger: z.looseObject({
|
|
229
|
+
entries: z.array(ledgerEntrySchema),
|
|
230
|
+
truncated: z.boolean(),
|
|
231
|
+
chain: z.looseObject({ ok: z.boolean(), entries: z.number(), through: z.number().nullable(), head: z.string(), problem: z.string().nullable(), checkedAt: z.string().nullable() }),
|
|
232
|
+
checkpoint: z.looseObject({ through: z.number(), hash: z.string(), at: z.string() }).nullable(),
|
|
233
|
+
recipe: z.string(),
|
|
234
|
+
}),
|
|
235
|
+
digest: z.string(),
|
|
236
|
+
});
|
|
237
|
+
/** `GET /flows/<id>?format=json`: the flow page's view (the same one the workspace renders). */
|
|
238
|
+
export const flowViewSchema = z.looseObject({
|
|
239
|
+
kind: z.literal("flow"),
|
|
240
|
+
flow: z.looseObject({ id: z.number(), name: z.string(), project: z.string(), revision: z.number(), href: z.string(), owner: z.string() }),
|
|
241
|
+
chatHref: z.string(),
|
|
242
|
+
triggers: z.array(z.looseObject({})),
|
|
243
|
+
triggerSetup: z.looseObject({}),
|
|
244
|
+
startTrigger: z.number().nullable(),
|
|
245
|
+
me: z.string(),
|
|
246
|
+
sortReady: z.boolean(),
|
|
247
|
+
emailReady: z.boolean(),
|
|
248
|
+
requestSecrets: z.array(z.string()),
|
|
249
|
+
tools: z.array(z.looseObject({ name: z.string() })),
|
|
250
|
+
scripts: z.array(z.looseObject({ name: z.string() })),
|
|
251
|
+
start: z.string(),
|
|
252
|
+
stages: z.array(z.looseObject({ id: z.string(), kind: z.string() })),
|
|
253
|
+
cards: z.array(z.looseObject({ id: z.number() })),
|
|
254
|
+
selectedCard: z.number().nullable(),
|
|
255
|
+
live: z.string().nullable(),
|
|
256
|
+
canEdit: z.boolean(),
|
|
257
|
+
approvers: z.array(z.string()),
|
|
258
|
+
kinds: z.array(z.looseObject({ kind: z.string(), label: z.string() })),
|
|
259
|
+
colors: z.array(z.string()),
|
|
260
|
+
});
|
|
261
|
+
export const CONSOLE_RESPONSES = {
|
|
262
|
+
ledgerPage: { route: "GET /ledger?format=json", schema: ledgerPageSchema },
|
|
263
|
+
evidencePack: { route: "GET /t/<task>/evidence?format=json", schema: evidencePackSchema },
|
|
264
|
+
flowView: { route: "GET /flows/<id>?format=json", schema: flowViewSchema },
|
|
265
|
+
};
|
|
266
|
+
/** Check a JSON response just before it is sent; a disagreement is reported, and the payload is returned unchanged. */
|
|
267
|
+
export function checkResponse(name, payload, report = toStderr) {
|
|
268
|
+
const { route, schema } = CONSOLE_RESPONSES[name];
|
|
269
|
+
const parsed = parseContract(schema, payload);
|
|
270
|
+
if (!parsed.ok)
|
|
271
|
+
report(route, parsed.issues);
|
|
272
|
+
return payload;
|
|
273
|
+
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The terminal diff-stat (`terminal-diff-stat.json`): one schema for what `parseNumstat` and `budgetedStatJson`
|
|
3
|
+
* write, and the two candidate-inventory reads that use it as a gate's evidence. Toolroll writes it from typed values
|
|
4
|
+
* in its own key order and never rewrites it, so the stored bytes (and every sha256 bound to them) stay as written.
|
|
5
|
+
*
|
|
6
|
+
* Both reads take only the fields their checks use and ignore the rest, exactly as the hand-written checks did: a
|
|
7
|
+
* legacy gate's endpoints, and a saved assessment's file list. Whether those fields match the run is plain code after
|
|
8
|
+
* parsing; anything the schema refuses was refused before too.
|
|
9
|
+
*/
|
|
10
|
+
import { z } from "zod";
|
|
11
|
+
import { type ContractResult } from "./contract.js";
|
|
12
|
+
/** One file's row. null additions/deletions = binary. */
|
|
13
|
+
export declare const diffStatFileSchema: z.ZodObject<{
|
|
14
|
+
path: z.ZodString;
|
|
15
|
+
additions: z.ZodNullable<z.ZodInt>;
|
|
16
|
+
deletions: z.ZodNullable<z.ZodInt>;
|
|
17
|
+
renamedFrom: z.ZodOptional<z.ZodString>;
|
|
18
|
+
}, z.core.$strip>;
|
|
19
|
+
export declare const diffStatSchema: z.ZodObject<{
|
|
20
|
+
schema: z.ZodLiteral<1>;
|
|
21
|
+
base: z.ZodString;
|
|
22
|
+
head: z.ZodString;
|
|
23
|
+
fileCount: z.ZodInt;
|
|
24
|
+
additions: z.ZodInt;
|
|
25
|
+
deletions: z.ZodInt;
|
|
26
|
+
binaryCount: z.ZodInt;
|
|
27
|
+
files: z.ZodArray<z.ZodObject<{
|
|
28
|
+
path: z.ZodString;
|
|
29
|
+
additions: z.ZodNullable<z.ZodInt>;
|
|
30
|
+
deletions: z.ZodNullable<z.ZodInt>;
|
|
31
|
+
renamedFrom: z.ZodOptional<z.ZodString>;
|
|
32
|
+
}, z.core.$strip>>;
|
|
33
|
+
filesTruncated: z.ZodBoolean;
|
|
34
|
+
}, z.core.$strip>;
|
|
35
|
+
export type DiffStat = z.infer<typeof diffStatSchema>;
|
|
36
|
+
export type DiffStatFile = z.infer<typeof diffStatFileSchema>;
|
|
37
|
+
/** What a legacy gate reads of its candidate inventory. `base` may be null, as a run without a base revision has. */
|
|
38
|
+
export declare const candidateEndpointsSchema: z.ZodObject<{
|
|
39
|
+
head: z.ZodString;
|
|
40
|
+
filesTruncated: z.ZodBoolean;
|
|
41
|
+
base: z.ZodNullable<z.ZodString>;
|
|
42
|
+
}, z.core.$strip>;
|
|
43
|
+
/** What a saved assessment reads: the inventory's identity and every changed path. */
|
|
44
|
+
export declare const savedInventorySchema: z.ZodObject<{
|
|
45
|
+
schema: z.ZodLiteral<1>;
|
|
46
|
+
base: z.ZodString;
|
|
47
|
+
head: z.ZodString;
|
|
48
|
+
fileCount: z.ZodInt;
|
|
49
|
+
filesTruncated: z.ZodBoolean;
|
|
50
|
+
files: z.ZodArray<z.ZodObject<{
|
|
51
|
+
path: z.ZodString;
|
|
52
|
+
}, z.core.$strip>>;
|
|
53
|
+
}, z.core.$strip>;
|
|
54
|
+
/** A legacy gate's endpoints, from the inventory's parsed JSON (the gate names unreadable JSON itself). */
|
|
55
|
+
export declare function readCandidateEndpoints(input: unknown): ContractResult<z.infer<typeof candidateEndpointsSchema>>;
|
|
56
|
+
/** The changed paths of a complete inventory for exactly this candidate, or null: wrong identity, a cut or
|
|
57
|
+
* miscounted list, or a path named twice. */
|
|
58
|
+
export declare function savedInventoryPaths(text: string, head: string, base: string): Set<string> | null;
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The terminal diff-stat (`terminal-diff-stat.json`): one schema for what `parseNumstat` and `budgetedStatJson`
|
|
3
|
+
* write, and the two candidate-inventory reads that use it as a gate's evidence. Toolroll writes it from typed values
|
|
4
|
+
* in its own key order and never rewrites it, so the stored bytes (and every sha256 bound to them) stay as written.
|
|
5
|
+
*
|
|
6
|
+
* Both reads take only the fields their checks use and ignore the rest, exactly as the hand-written checks did: a
|
|
7
|
+
* legacy gate's endpoints, and a saved assessment's file list. Whether those fields match the run is plain code after
|
|
8
|
+
* parsing; anything the schema refuses was refused before too.
|
|
9
|
+
*/
|
|
10
|
+
import { z } from "zod";
|
|
11
|
+
import { parseContract } from "./contract.js";
|
|
12
|
+
/** One file's row. null additions/deletions = binary. */
|
|
13
|
+
export const diffStatFileSchema = z.object({
|
|
14
|
+
path: z.string(),
|
|
15
|
+
additions: z.int().nullable(),
|
|
16
|
+
deletions: z.int().nullable(),
|
|
17
|
+
renamedFrom: z.string().optional(),
|
|
18
|
+
});
|
|
19
|
+
export const diffStatSchema = z.object({
|
|
20
|
+
schema: z.literal(1),
|
|
21
|
+
base: z.string(),
|
|
22
|
+
head: z.string(),
|
|
23
|
+
fileCount: z.int(),
|
|
24
|
+
additions: z.int(),
|
|
25
|
+
deletions: z.int(),
|
|
26
|
+
binaryCount: z.int(),
|
|
27
|
+
files: z.array(diffStatFileSchema),
|
|
28
|
+
/** True when the file list was cut to fit the cap — counts stay complete. */
|
|
29
|
+
filesTruncated: z.boolean(),
|
|
30
|
+
});
|
|
31
|
+
/** What a legacy gate reads of its candidate inventory. `base` may be null, as a run without a base revision has. */
|
|
32
|
+
export const candidateEndpointsSchema = diffStatSchema.pick({ head: true, filesTruncated: true }).extend({ base: z.string().nullable() });
|
|
33
|
+
/** What a saved assessment reads: the inventory's identity and every changed path. */
|
|
34
|
+
export const savedInventorySchema = diffStatSchema
|
|
35
|
+
.pick({ schema: true, head: true, base: true, filesTruncated: true, fileCount: true })
|
|
36
|
+
.extend({ files: z.array(diffStatFileSchema.pick({ path: true })) });
|
|
37
|
+
/** A legacy gate's endpoints, from the inventory's parsed JSON (the gate names unreadable JSON itself). */
|
|
38
|
+
export function readCandidateEndpoints(input) {
|
|
39
|
+
return parseContract(candidateEndpointsSchema, input);
|
|
40
|
+
}
|
|
41
|
+
/** The changed paths of a complete inventory for exactly this candidate, or null: wrong identity, a cut or
|
|
42
|
+
* miscounted list, or a path named twice. */
|
|
43
|
+
export function savedInventoryPaths(text, head, base) {
|
|
44
|
+
let input;
|
|
45
|
+
try {
|
|
46
|
+
input = JSON.parse(text);
|
|
47
|
+
}
|
|
48
|
+
catch {
|
|
49
|
+
return null;
|
|
50
|
+
}
|
|
51
|
+
const read = parseContract(savedInventorySchema, input);
|
|
52
|
+
if (!read.ok)
|
|
53
|
+
return null;
|
|
54
|
+
const inventory = read.value;
|
|
55
|
+
const paths = new Set(inventory.files.map(one => one.path));
|
|
56
|
+
if (inventory.head !== head || inventory.base !== base || inventory.filesTruncated || inventory.fileCount !== inventory.files.length || paths.size !== inventory.files.length)
|
|
57
|
+
return null;
|
|
58
|
+
return paths;
|
|
59
|
+
}
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Integration metadata (mcp-connect.ts, project-tools.ts): one schema each for what a service's sign-in says about
|
|
3
|
+
* itself — its protected-resource metadata, its authorization server's metadata, its answer to registering Toolroll,
|
|
4
|
+
* and its token answers — and for a project tool's spec.
|
|
5
|
+
*
|
|
6
|
+
* A server's answers are external, so they are read field by field and never strictly: an unknown key is ignored, a
|
|
7
|
+
* field of the wrong type reads as absent, and a list keeps the items of the right type (`scopes_supported` keeps the
|
|
8
|
+
* well-formed scope tokens). What JSON Schema doesn't say — https or loopback addresses, S256, how much scope fits a
|
|
9
|
+
* sign-in address, when a token expires — is checked by mcp-connect.ts after reading, as before.
|
|
10
|
+
*
|
|
11
|
+
* A tool spec is what `validateToolSpec` makes of a tool from any source (the catalog, the console, the lead, an
|
|
12
|
+
* import, a saved `project_tool` row). Saved specs were never versioned and every one is read by that adapter, which
|
|
13
|
+
* keeps its defaults, trimming, string-secret shorthand and plain-word refusals; the spec it returns is this schema.
|
|
14
|
+
*/
|
|
15
|
+
import { z } from "zod";
|
|
16
|
+
import { type ContractResult } from "./contract.js";
|
|
17
|
+
/** RFC 6749 scope tokens. */
|
|
18
|
+
export declare const SCOPE_TOKEN: RegExp;
|
|
19
|
+
/** RFC 9728 protected-resource metadata: the resource a token is for, who signs in for it, and the scopes it lists. */
|
|
20
|
+
export declare const protectedResourceSchema: z.ZodObject<{
|
|
21
|
+
resource: z.ZodOptional<z.ZodString>;
|
|
22
|
+
authorization_servers: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
23
|
+
scopes_supported: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
24
|
+
}, z.core.$strip>;
|
|
25
|
+
/** RFC 8414 (or OpenID) authorization-server metadata: where to sign in, trade a code, and register. */
|
|
26
|
+
export declare const authorizationServerSchema: z.ZodObject<{
|
|
27
|
+
authorization_endpoint: z.ZodOptional<z.ZodString>;
|
|
28
|
+
token_endpoint: z.ZodOptional<z.ZodString>;
|
|
29
|
+
registration_endpoint: z.ZodOptional<z.ZodString>;
|
|
30
|
+
code_challenge_methods_supported: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
31
|
+
}, z.core.$strip>;
|
|
32
|
+
/** RFC 7591 registration answer: the client Toolroll is, and its secret when the server gives one. */
|
|
33
|
+
export declare const registrationResponseSchema: z.ZodObject<{
|
|
34
|
+
client_id: z.ZodOptional<z.ZodString>;
|
|
35
|
+
client_secret: z.ZodOptional<z.ZodString>;
|
|
36
|
+
}, z.core.$strip>;
|
|
37
|
+
/** RFC 6749 §5.1/§5.2 token answer, for a code or a refresh: the tokens, how long they last, the scope granted, or the error. */
|
|
38
|
+
export declare const tokenResponseSchema: z.ZodObject<{
|
|
39
|
+
access_token: z.ZodOptional<z.ZodString>;
|
|
40
|
+
refresh_token: z.ZodOptional<z.ZodString>;
|
|
41
|
+
expires_in: z.ZodOptional<z.ZodNumber>;
|
|
42
|
+
scope: z.ZodOptional<z.ZodString>;
|
|
43
|
+
error: z.ZodOptional<z.ZodString>;
|
|
44
|
+
}, z.core.$strip>;
|
|
45
|
+
export type ProtectedResource = z.infer<typeof protectedResourceSchema>;
|
|
46
|
+
export type AuthorizationServer = z.infer<typeof authorizationServerSchema>;
|
|
47
|
+
export type RegistrationResponse = z.infer<typeof registrationResponseSchema>;
|
|
48
|
+
/** `scopeSaid`: the answer has a `scope` key at all. Absent, an initial grant is what was asked for and a refresh
|
|
49
|
+
* keeps the grant it had; present but not scope text, the grant is unknown. */
|
|
50
|
+
export type TokenResponse = z.infer<typeof tokenResponseSchema> & {
|
|
51
|
+
scopeSaid: boolean;
|
|
52
|
+
};
|
|
53
|
+
export declare const readProtectedResource: (body: Record<string, unknown>) => ProtectedResource;
|
|
54
|
+
export declare const readAuthorizationServer: (body: Record<string, unknown>) => AuthorizationServer;
|
|
55
|
+
/** A registration answer's JSON; null when the server answered `null`, which has always read as no answer. Any other
|
|
56
|
+
* non-object (a list, a string) carries no client. */
|
|
57
|
+
export declare const readRegistration: (body: unknown) => RegistrationResponse | null;
|
|
58
|
+
/** A token answer's JSON; null when the server answered `null` (no answer), as for a registration. */
|
|
59
|
+
export declare const readTokenResponse: (body: unknown) => TokenResponse | null;
|
|
60
|
+
export declare const TOOL_NAME: RegExp;
|
|
61
|
+
export declare const SECRET_NAME: RegExp;
|
|
62
|
+
export declare const HEADER_NAME: RegExp;
|
|
63
|
+
/** How long a tool's settings may be, and how many arguments and secrets it names. `about` is clipped to its limit
|
|
64
|
+
* when given; the one made for a tool without one names its host or program, however long. */
|
|
65
|
+
export declare const TOOL_SPEC_LIMITS: {
|
|
66
|
+
readonly name: 40;
|
|
67
|
+
readonly command: 400;
|
|
68
|
+
readonly arg: 400;
|
|
69
|
+
readonly args: 40;
|
|
70
|
+
readonly url: 500;
|
|
71
|
+
readonly about: 240;
|
|
72
|
+
readonly secrets: 12;
|
|
73
|
+
};
|
|
74
|
+
/** One secret a tool needs: an env name for a local server, or the value behind an http header. */
|
|
75
|
+
export declare const toolSecretSchema: z.ZodObject<{
|
|
76
|
+
name: z.ZodString;
|
|
77
|
+
optional: z.ZodBoolean;
|
|
78
|
+
}, z.core.$strict>;
|
|
79
|
+
export declare const toolSpecSchema: z.ZodObject<{
|
|
80
|
+
name: z.ZodString;
|
|
81
|
+
transport: z.ZodEnum<{
|
|
82
|
+
http: "http";
|
|
83
|
+
stdio: "stdio";
|
|
84
|
+
}>;
|
|
85
|
+
command: z.ZodNullable<z.ZodString>;
|
|
86
|
+
args: z.ZodArray<z.ZodString>;
|
|
87
|
+
url: z.ZodNullable<z.ZodString>;
|
|
88
|
+
secrets: z.ZodArray<z.ZodObject<{
|
|
89
|
+
name: z.ZodString;
|
|
90
|
+
optional: z.ZodBoolean;
|
|
91
|
+
}, z.core.$strict>>;
|
|
92
|
+
bearer: z.ZodNullable<z.ZodString>;
|
|
93
|
+
headerSecrets: z.ZodRecord<z.ZodString, z.ZodString>;
|
|
94
|
+
about: z.ZodString;
|
|
95
|
+
}, z.core.$strict>;
|
|
96
|
+
export type ToolSecret = z.infer<typeof toolSecretSchema>;
|
|
97
|
+
export type ToolSpec = z.infer<typeof toolSpecSchema>;
|
|
98
|
+
/** A spec as the tool adapter made it, checked against the schema (path-named lines when it isn't one). */
|
|
99
|
+
export declare const readToolSpec: (input: unknown) => ContractResult<ToolSpec>;
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Integration metadata (mcp-connect.ts, project-tools.ts): one schema each for what a service's sign-in says about
|
|
3
|
+
* itself — its protected-resource metadata, its authorization server's metadata, its answer to registering Toolroll,
|
|
4
|
+
* and its token answers — and for a project tool's spec.
|
|
5
|
+
*
|
|
6
|
+
* A server's answers are external, so they are read field by field and never strictly: an unknown key is ignored, a
|
|
7
|
+
* field of the wrong type reads as absent, and a list keeps the items of the right type (`scopes_supported` keeps the
|
|
8
|
+
* well-formed scope tokens). What JSON Schema doesn't say — https or loopback addresses, S256, how much scope fits a
|
|
9
|
+
* sign-in address, when a token expires — is checked by mcp-connect.ts after reading, as before.
|
|
10
|
+
*
|
|
11
|
+
* A tool spec is what `validateToolSpec` makes of a tool from any source (the catalog, the console, the lead, an
|
|
12
|
+
* import, a saved `project_tool` row). Saved specs were never versioned and every one is read by that adapter, which
|
|
13
|
+
* keeps its defaults, trimming, string-secret shorthand and plain-word refusals; the spec it returns is this schema.
|
|
14
|
+
*/
|
|
15
|
+
import { z } from "zod";
|
|
16
|
+
import { parseContract } from "./contract.js";
|
|
17
|
+
/** RFC 6749 scope tokens. */
|
|
18
|
+
export const SCOPE_TOKEN = /^[\x21\x23-\x5b\x5d-\x7e]{1,128}$/;
|
|
19
|
+
const isRecord = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
20
|
+
/** A field's schema without its optional wrapper. */
|
|
21
|
+
const inner = (field) => (field instanceof z.ZodOptional ? field.unwrap() : field);
|
|
22
|
+
/**
|
|
23
|
+
* An external object read by `schema`, field by field: an absent or wrong-typed field is left out, a list keeps the
|
|
24
|
+
* items its schema takes, and keys the schema doesn't name are ignored.
|
|
25
|
+
*/
|
|
26
|
+
function fieldsOf(schema, input) {
|
|
27
|
+
const out = {};
|
|
28
|
+
for (const [key, field] of Object.entries(schema.shape)) {
|
|
29
|
+
const value = input[key];
|
|
30
|
+
if (value === undefined)
|
|
31
|
+
continue;
|
|
32
|
+
const of = inner(field);
|
|
33
|
+
if (of instanceof z.ZodArray && Array.isArray(value))
|
|
34
|
+
out[key] = value.filter(item => of.element.safeParse(item).success);
|
|
35
|
+
else if (field.safeParse(value).success)
|
|
36
|
+
out[key] = value;
|
|
37
|
+
}
|
|
38
|
+
return out;
|
|
39
|
+
}
|
|
40
|
+
// ---- a service's sign-in (OAuth) -------------------------------------------------------------------------------
|
|
41
|
+
/** RFC 9728 protected-resource metadata: the resource a token is for, who signs in for it, and the scopes it lists. */
|
|
42
|
+
export const protectedResourceSchema = z.object({
|
|
43
|
+
resource: z.string().optional(),
|
|
44
|
+
authorization_servers: z.array(z.string()).optional(),
|
|
45
|
+
scopes_supported: z.array(z.string().regex(SCOPE_TOKEN)).optional(),
|
|
46
|
+
});
|
|
47
|
+
/** RFC 8414 (or OpenID) authorization-server metadata: where to sign in, trade a code, and register. */
|
|
48
|
+
export const authorizationServerSchema = z.object({
|
|
49
|
+
authorization_endpoint: z.string().optional(),
|
|
50
|
+
token_endpoint: z.string().optional(),
|
|
51
|
+
registration_endpoint: z.string().optional(),
|
|
52
|
+
code_challenge_methods_supported: z.array(z.string()).optional(),
|
|
53
|
+
});
|
|
54
|
+
/** RFC 7591 registration answer: the client Toolroll is, and its secret when the server gives one. */
|
|
55
|
+
export const registrationResponseSchema = z.object({
|
|
56
|
+
client_id: z.string().optional(),
|
|
57
|
+
client_secret: z.string().optional(),
|
|
58
|
+
});
|
|
59
|
+
/** RFC 6749 §5.1/§5.2 token answer, for a code or a refresh: the tokens, how long they last, the scope granted, or the error. */
|
|
60
|
+
export const tokenResponseSchema = z.object({
|
|
61
|
+
access_token: z.string().optional(),
|
|
62
|
+
refresh_token: z.string().optional(),
|
|
63
|
+
expires_in: z.number().positive().optional(),
|
|
64
|
+
scope: z.string().optional(),
|
|
65
|
+
error: z.string().optional(),
|
|
66
|
+
});
|
|
67
|
+
export const readProtectedResource = (body) => fieldsOf(protectedResourceSchema, body);
|
|
68
|
+
export const readAuthorizationServer = (body) => fieldsOf(authorizationServerSchema, body);
|
|
69
|
+
/** A registration answer's JSON; null when the server answered `null`, which has always read as no answer. Any other
|
|
70
|
+
* non-object (a list, a string) carries no client. */
|
|
71
|
+
export const readRegistration = (body) => body === null ? null : isRecord(body) ? fieldsOf(registrationResponseSchema, body) : {};
|
|
72
|
+
/** A token answer's JSON; null when the server answered `null` (no answer), as for a registration. */
|
|
73
|
+
export const readTokenResponse = (body) => body === null ? null : isRecord(body) ? { ...fieldsOf(tokenResponseSchema, body), scopeSaid: "scope" in body } : { scopeSaid: false };
|
|
74
|
+
// ---- a project's tool -----------------------------------------------------------------------------------------
|
|
75
|
+
export const TOOL_NAME = /^[a-z0-9][a-z0-9_-]{0,39}$/;
|
|
76
|
+
export const SECRET_NAME = /^[A-Z][A-Z0-9_]{0,63}$/;
|
|
77
|
+
export const HEADER_NAME = /^[A-Za-z0-9-]{1,64}$/;
|
|
78
|
+
/** How long a tool's settings may be, and how many arguments and secrets it names. `about` is clipped to its limit
|
|
79
|
+
* when given; the one made for a tool without one names its host or program, however long. */
|
|
80
|
+
export const TOOL_SPEC_LIMITS = { name: 40, command: 400, arg: 400, args: 40, url: 500, about: 240, secrets: 12 };
|
|
81
|
+
/** One secret a tool needs: an env name for a local server, or the value behind an http header. */
|
|
82
|
+
export const toolSecretSchema = z.strictObject({ name: z.string().regex(SECRET_NAME), optional: z.boolean() });
|
|
83
|
+
export const toolSpecSchema = z.strictObject({
|
|
84
|
+
name: z.string().regex(TOOL_NAME),
|
|
85
|
+
transport: z.enum(["stdio", "http"]),
|
|
86
|
+
/** stdio: the program and its arguments (no secrets in either). */
|
|
87
|
+
command: z.string().max(TOOL_SPEC_LIMITS.command).nullable(),
|
|
88
|
+
args: z.array(z.string().max(TOOL_SPEC_LIMITS.arg)).max(TOOL_SPEC_LIMITS.args),
|
|
89
|
+
/** http: the server's address. */
|
|
90
|
+
url: z.string().max(TOOL_SPEC_LIMITS.url).nullable(),
|
|
91
|
+
/** Every secret the tool needs. stdio: each is an env variable of the server process. */
|
|
92
|
+
secrets: z.array(toolSecretSchema).max(TOOL_SPEC_LIMITS.secrets),
|
|
93
|
+
/** http: the secret sent as `Authorization: Bearer <value>`. */
|
|
94
|
+
bearer: z.string().regex(SECRET_NAME).nullable(),
|
|
95
|
+
/** http: header name (HEADER_NAME, checked by the adapter: a key pattern has no exact JSON Schema) → the secret sent as its raw value. */
|
|
96
|
+
headerSecrets: z.record(z.string(), z.string().regex(SECRET_NAME)),
|
|
97
|
+
/** What it does, in plain words. */
|
|
98
|
+
about: z.string(),
|
|
99
|
+
});
|
|
100
|
+
/** A spec as the tool adapter made it, checked against the schema (path-named lines when it isn't one). */
|
|
101
|
+
export const readToolSpec = (input) => parseContract(toolSpecSchema, input);
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What the model catalog (model-catalog.ts) reads from outside Toolroll: the Codex CLI's own model list
|
|
3
|
+
* (`~/.codex/models_cache.json`) and npm's latest-version answer for each agent CLI. Both belong to someone else, so
|
|
4
|
+
* both are read loosely: unknown keys are ignored, an entry that doesn't fit is skipped, and a file or answer that
|
|
5
|
+
* doesn't fit at all reads as nothing (an empty list, no version) — never an error.
|
|
6
|
+
*/
|
|
7
|
+
import { z } from "zod";
|
|
8
|
+
/** One listed Codex model: a usable id, shown in the picker. Its display name is used when it is text. */
|
|
9
|
+
export declare const codexModelSchema: z.ZodObject<{
|
|
10
|
+
slug: z.ZodString;
|
|
11
|
+
visibility: z.ZodLiteral<"list">;
|
|
12
|
+
display_name: z.ZodOptional<z.ZodUnknown>;
|
|
13
|
+
}, z.core.$strip>;
|
|
14
|
+
/** The Codex cache file: its `models` list, entries read one by one with `codexModelSchema`. */
|
|
15
|
+
export declare const codexModelsCacheSchema: z.ZodObject<{
|
|
16
|
+
models: z.ZodArray<z.ZodUnknown>;
|
|
17
|
+
}, z.core.$strip>;
|
|
18
|
+
/** npm's `/<package>/latest` answer: a version starting with major.minor.patch. */
|
|
19
|
+
export declare const npmLatestSchema: z.ZodObject<{
|
|
20
|
+
version: z.ZodString;
|
|
21
|
+
}, z.core.$strip>;
|