@intentic/sandbox-contract 1.126.0 → 1.133.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/agent-catalog.d.ts.map +1 -1
- package/dist/contracts/activity.contract.d.ts +4 -4
- package/dist/contracts/activity.contract.d.ts.map +1 -1
- package/dist/contracts/agent.contract.d.ts +283 -252
- package/dist/contracts/agent.contract.d.ts.map +1 -1
- package/dist/contracts/agent.contract.js +4 -3
- package/dist/contracts/agent.contract.js.map +1 -1
- package/dist/contracts/agents.contract.d.ts +15 -13
- package/dist/contracts/agents.contract.d.ts.map +1 -1
- package/dist/contracts/automations.contract.d.ts +3 -3
- package/dist/contracts/automations.contract.d.ts.map +1 -1
- package/dist/contracts/capabilities.contract.d.ts +21 -16
- package/dist/contracts/capabilities.contract.d.ts.map +1 -1
- package/dist/contracts/claude.contract.d.ts.map +1 -1
- package/dist/contracts/codex.contract.d.ts.map +1 -1
- package/dist/contracts/drafts.contract.d.ts +6 -6
- package/dist/contracts/drafts.contract.d.ts.map +1 -1
- package/dist/contracts/extensions.contract.d.ts +4 -4
- package/dist/contracts/extensions.contract.d.ts.map +1 -1
- package/dist/contracts/git.contract.d.ts +6 -4
- package/dist/contracts/git.contract.d.ts.map +1 -1
- package/dist/contracts/grok.contract.d.ts.map +1 -1
- package/dist/contracts/history.contract.d.ts +3 -3
- package/dist/contracts/history.contract.d.ts.map +1 -1
- package/dist/contracts/intentic.contract.d.ts.map +1 -1
- package/dist/contracts/inventory.contract.d.ts +20 -20
- package/dist/contracts/inventory.contract.d.ts.map +1 -1
- package/dist/contracts/logs.contract.d.ts.map +1 -1
- package/dist/contracts/panels.contract.d.ts +1 -1
- package/dist/contracts/panels.contract.d.ts.map +1 -1
- package/dist/contracts/ports.contract.d.ts +1 -1
- package/dist/contracts/ports.contract.d.ts.map +1 -1
- package/dist/contracts/secrets.contract.d.ts +4 -4
- package/dist/contracts/secrets.contract.d.ts.map +1 -1
- package/dist/contracts/sessions.contract.d.ts +1 -1
- package/dist/contracts/sessions.contract.d.ts.map +1 -1
- package/dist/contracts/settings.contract.d.ts.map +1 -1
- package/dist/contracts/system.contract.d.ts +26 -26
- package/dist/contracts/system.contract.d.ts.map +1 -1
- package/dist/contracts/translator.contract.d.ts.map +1 -1
- package/dist/contracts/workspace.contract.d.ts +21 -21
- package/dist/contracts/workspace.contract.d.ts.map +1 -1
- package/dist/effects.d.ts +1 -1
- package/dist/effects.d.ts.map +1 -1
- package/dist/effects.js +2 -0
- package/dist/effects.js.map +1 -1
- package/dist/events.d.ts +213 -29
- package/dist/events.d.ts.map +1 -1
- package/dist/events.js +5 -0
- package/dist/events.js.map +1 -1
- package/dist/hostnames.d.ts.map +1 -1
- package/dist/index.d.ts +417 -377
- package/dist/index.d.ts.map +1 -1
- package/dist/model-metadata.d.ts.map +1 -1
- package/dist/schemas.d.ts +221 -212
- package/dist/schemas.d.ts.map +1 -1
- package/dist/schemas.js +13 -6
- package/dist/schemas.js.map +1 -1
- package/dist/sse.d.ts.map +1 -1
- package/dist/tunnel-ids.d.ts.map +1 -1
- package/package.json +8 -6
- package/src/agent-catalog.ts +62 -0
- package/src/contracts/activity.contract.ts +9 -0
- package/src/contracts/agent.contract.ts +17 -0
- package/src/contracts/agents.contract.ts +28 -0
- package/src/contracts/automations.contract.ts +23 -0
- package/src/contracts/capabilities.contract.ts +31 -0
- package/src/contracts/claude.contract.ts +22 -0
- package/src/contracts/codex.contract.ts +23 -0
- package/src/contracts/drafts.contract.ts +11 -0
- package/src/contracts/extensions.contract.ts +28 -0
- package/src/contracts/git.contract.ts +64 -0
- package/src/contracts/grok.contract.ts +16 -0
- package/src/contracts/history.contract.ts +12 -0
- package/src/contracts/intentic.contract.ts +17 -0
- package/src/contracts/inventory.contract.ts +11 -0
- package/src/contracts/logs.contract.ts +9 -0
- package/src/contracts/panels.contract.ts +12 -0
- package/src/contracts/ports.contract.ts +11 -0
- package/src/contracts/secrets.contract.ts +15 -0
- package/src/contracts/sessions.contract.ts +14 -0
- package/src/contracts/settings.contract.ts +11 -0
- package/src/contracts/system.contract.ts +37 -0
- package/src/contracts/translator.contract.ts +20 -0
- package/src/contracts/workspace.contract.ts +74 -0
- package/src/effects.test.ts +132 -0
- package/src/effects.ts +138 -0
- package/src/events.ts +277 -0
- package/src/hostnames.test.ts +44 -0
- package/src/hostnames.ts +97 -0
- package/src/index.ts +84 -0
- package/src/model-metadata.ts +30 -0
- package/src/schemas.ts +1407 -0
- package/src/sse.ts +43 -0
- package/src/terminal-protocol.ts +16 -0
- package/src/tunnel-ids.ts +19 -0
package/src/schemas.ts
ADDED
|
@@ -0,0 +1,1407 @@
|
|
|
1
|
+
import { ExtensionManifestSchema } from "@intentic/extension-api";
|
|
2
|
+
import { z } from "zod";
|
|
3
|
+
|
|
4
|
+
// All request/response wire schemas for the sandbox daemon. Inputs that carry a `{param}` in their route path
|
|
5
|
+
// (repo / id / name) merge the path param into the same flat object — oRPC fills the path placeholder from the
|
|
6
|
+
// matching key and routes the rest to the body (POST/PUT) or query (GET).
|
|
7
|
+
|
|
8
|
+
// ---- shared ----
|
|
9
|
+
|
|
10
|
+
// Success ack for routes that only report completion (push / disconnect / self-host register). A turn paused on
|
|
11
|
+
// a plan/question that no longer exists, or a missing repo/path, is an ORPCError thrown by the handler instead.
|
|
12
|
+
export const OkSchema = z.object({ ok: z.literal(true) });
|
|
13
|
+
|
|
14
|
+
// Which repo a git route targets: "root" (the /work workspace repo) or a repo id — the repo's root-relative
|
|
15
|
+
// dir, which may be nested ("clients/foo"; URL-encoded in the path param). Kept as a bare string on the wire
|
|
16
|
+
// (not an enum) so an unknown repo is a handler-thrown NOT_FOUND — matching the daemon's prior 404 — rather
|
|
17
|
+
// than an input-validation rejection.
|
|
18
|
+
export const RepoParamSchema = z.object({ repo: z.string() });
|
|
19
|
+
|
|
20
|
+
// ---- agent ----
|
|
21
|
+
|
|
22
|
+
export const SessionTranscriptMessageSchema = z.object({ role: z.enum(["user", "assistant"]), text: z.string() });
|
|
23
|
+
export type SessionTranscriptMessage = z.infer<typeof SessionTranscriptMessageSchema>;
|
|
24
|
+
|
|
25
|
+
// The agent runtimes the daemon can serve — the vocabulary every surface that picks an agent shares (chat
|
|
26
|
+
// turns, automations). The three NATIVE providers have dedicated adapters (and their ids are reserved); any
|
|
27
|
+
// other value is the id of an installed `agent`-kind capability served over ACP (Agent Client Protocol).
|
|
28
|
+
// Kept as a bare string on the wire (not an enum) so an unknown id is a clean error frame from the agent
|
|
29
|
+
// route — the same bet RepoParamSchema makes — and adding an ACP agent needs no contract change.
|
|
30
|
+
export const NATIVE_PROVIDERS = ["claude", "codex", "grok"] as const;
|
|
31
|
+
export type NativeProvider = (typeof NATIVE_PROVIDERS)[number];
|
|
32
|
+
export const AgentProviderSchema = z.string().min(1);
|
|
33
|
+
export type AgentProvider = z.infer<typeof AgentProviderSchema>;
|
|
34
|
+
|
|
35
|
+
// The harness (agentic loop) a turn runs on, orthogonal to the provider. See AgentTurnSchema.harness.
|
|
36
|
+
export const AgentHarnessSchema = z.enum(["native", "claude-code"]);
|
|
37
|
+
export type AgentHarness = z.infer<typeof AgentHarnessSchema>;
|
|
38
|
+
|
|
39
|
+
// What the user is looking at in the editor, attached to a turn only when they explicitly opt in (the
|
|
40
|
+
// composer chip — off by default). The daemon folds it into the prompt as a context note, so deictic
|
|
41
|
+
// prompts ("fix this") resolve without an @-mention. Selection is bounded — it's context, not an upload.
|
|
42
|
+
export const EditorContextSchema = z.object({
|
|
43
|
+
// Workspace-relative path of the file open in the editor.
|
|
44
|
+
file: z.string().min(1),
|
|
45
|
+
// 1-based line range of the selection; absent when the whole file is the context.
|
|
46
|
+
startLine: z.number().int().min(1).optional(),
|
|
47
|
+
endLine: z.number().int().min(1).optional(),
|
|
48
|
+
// The selected text itself, truncated client-side to the cap.
|
|
49
|
+
selection: z.string().max(20_000).optional(),
|
|
50
|
+
});
|
|
51
|
+
export type EditorContext = z.infer<typeof EditorContextSchema>;
|
|
52
|
+
|
|
53
|
+
// The client-minted stable conversation identity. Constrained because it lands in branch names (agent/<id>)
|
|
54
|
+
// and filesystem paths — the regex is the injection guard. Shared by the turn input and the attach input.
|
|
55
|
+
const ConversationIdSchema = z.string().regex(/^[a-zA-Z0-9][a-zA-Z0-9_-]{0,63}$/);
|
|
56
|
+
|
|
57
|
+
export const AgentTurnSchema = z
|
|
58
|
+
.object({
|
|
59
|
+
prompt: z.string(),
|
|
60
|
+
// The client's display title for the conversation — seeds a FRESH registry entry (so a renamed draft's
|
|
61
|
+
// first turn keeps its user-chosen title); an existing entry's title always wins.
|
|
62
|
+
title: z.string().max(80).optional(),
|
|
63
|
+
// Workspace-relative paths of files the user attached, already uploaded via /workspace/upload
|
|
64
|
+
// (the browser puts them under .intentic/attachments/<uuid>/<name>). The daemon hands them to the
|
|
65
|
+
// provider: Claude reads them from disk via its Read tool; Codex gets images as native inputs.
|
|
66
|
+
attachments: z.array(z.string().min(1)).max(20).optional(),
|
|
67
|
+
// Which provider (model + account) serves the turn; absent = claude. A sessionId only resumes on the
|
|
68
|
+
// provider that minted it (Claude Code sessions vs Codex threads vs Grok/OpenCode sessions are separate
|
|
69
|
+
// stores) — a mid-conversation provider/account/harness switch sends `history` instead of resuming.
|
|
70
|
+
agent: AgentProviderSchema.optional(),
|
|
71
|
+
// Which harness (agentic loop) runs the turn, orthogonal to the provider above. Absent = "native": each
|
|
72
|
+
// provider on its own runtime (Claude Code SDK / Codex CLI / opencode) with its subscription OAuth.
|
|
73
|
+
// "claude-code" forces the Claude Code Agent SDK loop for ANY provider — codex/grok then drive their model
|
|
74
|
+
// through the sandbox's bundled Anthropic↔OpenAI translator, which needs that provider's API key (its
|
|
75
|
+
// subscription OAuth can't reach a gateway). For the claude provider the two are identical.
|
|
76
|
+
harness: AgentHarnessSchema.optional(),
|
|
77
|
+
// Which connected account of that provider serves the turn; absent = the provider's first account.
|
|
78
|
+
account: z.string().optional(),
|
|
79
|
+
sessionId: z.string().optional(),
|
|
80
|
+
// The client-minted stable conversation identity (survives provider/account/harness switches, which
|
|
81
|
+
// retire sessions). Keys the fleet registry entry, the conversation's worktree, and the turn run.
|
|
82
|
+
conversationId: ConversationIdSchema.optional(),
|
|
83
|
+
// When true, the turn runs in the conversation's isolated git worktree (created lazily on first use)
|
|
84
|
+
// instead of the shared /work tree — the parallel-agents mode. Requires conversationId.
|
|
85
|
+
isolated: z.boolean().optional(),
|
|
86
|
+
// The client-held transcript of a conversation that just switched provider/account: seeds the FIRST
|
|
87
|
+
// turn of the replacement session. The daemon folds it into the prompt as one role-attributed context
|
|
88
|
+
// preamble for every runtime. Mutually exclusive with sessionId — a resumed session has its context.
|
|
89
|
+
history: z.array(SessionTranscriptMessageSchema).max(200).optional(),
|
|
90
|
+
// The browser sends the chosen model per turn; the provider token is the sandbox's own stored credential.
|
|
91
|
+
model: z.string().optional(),
|
|
92
|
+
// When true, run the always-plan flow (propose → approve → execute). Reasoning controls are optional.
|
|
93
|
+
plan: z.boolean().optional(),
|
|
94
|
+
effort: z.string().optional(),
|
|
95
|
+
thinking: z.boolean().optional(),
|
|
96
|
+
// The opt-in editor context chip: what the user is looking at, folded into the prompt daemon-side.
|
|
97
|
+
editorContext: EditorContextSchema.optional(),
|
|
98
|
+
})
|
|
99
|
+
// An attachment-only send (no text) is legal; an entirely empty turn is not.
|
|
100
|
+
.refine((turn) => turn.prompt.trim().length > 0 || (turn.attachments?.length ?? 0) > 0, {
|
|
101
|
+
message: "prompt or attachments required",
|
|
102
|
+
})
|
|
103
|
+
.refine((turn) => turn.sessionId === undefined || turn.history === undefined, {
|
|
104
|
+
message: "history and sessionId are mutually exclusive",
|
|
105
|
+
})
|
|
106
|
+
.refine((turn) => turn.isolated !== true || turn.conversationId !== undefined, {
|
|
107
|
+
message: "isolated requires conversationId",
|
|
108
|
+
});
|
|
109
|
+
export type AgentTurn = z.infer<typeof AgentTurnSchema>;
|
|
110
|
+
|
|
111
|
+
// POST /agent's ack: the daemon-minted id of the detached turn run it started. The turn executes daemon-side
|
|
112
|
+
// regardless of any client connection; every window — the initiator included — renders it via /agent/attach.
|
|
113
|
+
export const StartedTurnSchema = z.object({ run: z.string() });
|
|
114
|
+
export type StartedTurn = z.infer<typeof StartedTurnSchema>;
|
|
115
|
+
|
|
116
|
+
// Attach to a conversation's turn run (live, or finished within the retention window). `run`+`after` is the
|
|
117
|
+
// resume cursor of a client whose stream dropped: frames after `after` replay when `run` still names the
|
|
118
|
+
// current run; a mismatch (a newer turn started meanwhile) replays that run from its first frame instead.
|
|
119
|
+
export const AttachTurnSchema = z.object({
|
|
120
|
+
conversationId: ConversationIdSchema,
|
|
121
|
+
run: z.string().optional(),
|
|
122
|
+
after: z.number().int().min(0).optional(),
|
|
123
|
+
});
|
|
124
|
+
export type AttachTurn = z.infer<typeof AttachTurnSchema>;
|
|
125
|
+
|
|
126
|
+
// ---- agents: the parallel-conversation fleet ----
|
|
127
|
+
// A "fleet agent" is a conversation with a registry entry — every isolated conversation, keyed by its
|
|
128
|
+
// conversationId. Isolated ones own a git worktree (branch agent/<id> in every workspace repo); the fleet
|
|
129
|
+
// surface shows all of them with live status/activity/cost so the user can drive N agents in parallel.
|
|
130
|
+
|
|
131
|
+
// idle/running/awaiting are the turn lifecycle (awaiting = paused on a plan approval or question); landed /
|
|
132
|
+
// conflict are outcomes of the land flow; error is a terminal turn failure surfaced on the card.
|
|
133
|
+
export const AgentStatusSchema = z.enum(["idle", "running", "awaiting", "landed", "conflict", "error"]);
|
|
134
|
+
export type AgentStatus = z.infer<typeof AgentStatusSchema>;
|
|
135
|
+
// The card's live activity snippet: the last tool the agent used (with its target) and the in-progress todo.
|
|
136
|
+
export const AgentActivitySchema = z.object({
|
|
137
|
+
tool: z.string().optional(),
|
|
138
|
+
target: z.string().optional(),
|
|
139
|
+
todo: z.string().optional(),
|
|
140
|
+
});
|
|
141
|
+
export type AgentActivity = z.infer<typeof AgentActivitySchema>;
|
|
142
|
+
// Which "needs you" flags are raised — the fleet badge aggregates these across all agents.
|
|
143
|
+
export const AgentAttentionSchema = z.object({ plan: z.boolean(), question: z.boolean(), conflict: z.boolean() });
|
|
144
|
+
export type AgentAttention = z.infer<typeof AgentAttentionSchema>;
|
|
145
|
+
export const AgentSummarySchema = z.object({
|
|
146
|
+
// The conversationId.
|
|
147
|
+
id: z.string(),
|
|
148
|
+
sessionId: z.string().optional(),
|
|
149
|
+
// First prompt, sanitized to one bounded line.
|
|
150
|
+
title: z.string().optional(),
|
|
151
|
+
status: AgentStatusSchema,
|
|
152
|
+
provider: AgentProviderSchema,
|
|
153
|
+
harness: AgentHarnessSchema,
|
|
154
|
+
model: z.string().optional(),
|
|
155
|
+
account: z.string().optional(),
|
|
156
|
+
// The worktree branch (agent/<id>); absent for a non-isolated (main-tree) conversation.
|
|
157
|
+
branch: z.string().optional(),
|
|
158
|
+
// The ROOT repo's short base sha — the checkout moment's display identity. Per-repo bases stay
|
|
159
|
+
// daemon-internal (agents.diff already reports against them).
|
|
160
|
+
base: z.string().optional(),
|
|
161
|
+
costUsd: z.number().optional(),
|
|
162
|
+
inputTokens: z.number().optional(),
|
|
163
|
+
outputTokens: z.number().optional(),
|
|
164
|
+
contextTokens: z.number().optional(),
|
|
165
|
+
contextWindow: z.number().optional(),
|
|
166
|
+
activity: AgentActivitySchema.optional(),
|
|
167
|
+
// Present while a turn runs: its start, ms since epoch.
|
|
168
|
+
startedAt: z.number().optional(),
|
|
169
|
+
updatedAt: z.number(),
|
|
170
|
+
attention: AgentAttentionSchema,
|
|
171
|
+
// Completed turns and lifetime tool calls — the card's msgs/tools counters.
|
|
172
|
+
turns: z.number().optional(),
|
|
173
|
+
toolUses: z.number().optional(),
|
|
174
|
+
// The agent's cumulative output (base → branch tip across every repo), refreshed on each land —
|
|
175
|
+
// the card's "12 files · +412 −96" readout. Independent of what has landed.
|
|
176
|
+
diff: z.object({ files: z.number(), insertions: z.number(), deletions: z.number() }).optional(),
|
|
177
|
+
});
|
|
178
|
+
export type AgentSummary = z.infer<typeof AgentSummarySchema>;
|
|
179
|
+
export const AgentsListSchema = z.object({ agents: z.array(AgentSummarySchema) });
|
|
180
|
+
export const AgentIdSchema = z.object({ id: z.string().min(1) });
|
|
181
|
+
// rename's input: the user-chosen display title (bounded like sanitizeTitle's cap).
|
|
182
|
+
export const AgentRenameSchema = z.object({ id: z.string().min(1), title: z.string().trim().min(1).max(80) });
|
|
183
|
+
export const AgentFileDiffQuerySchema = z.object({ id: z.string().min(1), repo: z.string().min(1), path: z.string().min(1) });
|
|
184
|
+
// land's outcome: per-repo conflicts (dirty-main overlaps or merge conflicts); landed only when every repo
|
|
185
|
+
// with changes merged cleanly. Conflicted repos keep their worktree state — nothing is lost.
|
|
186
|
+
export const LandResultSchema = z.object({
|
|
187
|
+
landed: z.boolean(),
|
|
188
|
+
conflicts: z.array(z.object({ repo: z.string(), paths: z.array(z.string()) })).optional(),
|
|
189
|
+
});
|
|
190
|
+
export type LandResult = z.infer<typeof LandResultSchema>;
|
|
191
|
+
|
|
192
|
+
// ---- routed-provider subscriptions ----
|
|
193
|
+
|
|
194
|
+
// The providers whose model can run UNDER the Claude Code harness through the bundled translator (CLIProxyAPI),
|
|
195
|
+
// which holds their SUBSCRIPTION OAuth and re-serves it behind an Anthropic endpoint. The `claude` provider is
|
|
196
|
+
// absent — native Anthropic OAuth serves it directly, without the translator.
|
|
197
|
+
export const KeyedProviderSchema = z.enum(["codex", "grok"]);
|
|
198
|
+
export type KeyedProvider = z.infer<typeof KeyedProviderSchema>;
|
|
199
|
+
|
|
200
|
+
// Which routed-provider subscriptions are connected in the translator (per provider). Drives the
|
|
201
|
+
// "connected / connect subscription" state in Sandbox ▸ Agent.
|
|
202
|
+
export const TranslatorAccountsSchema = z.object({ codex: z.boolean(), grok: z.boolean() });
|
|
203
|
+
export type TranslatorAccounts = z.infer<typeof TranslatorAccountsSchema>;
|
|
204
|
+
|
|
205
|
+
// Side-channel bodies: the UI posts these to resolve a turn paused on a plan approval / question.
|
|
206
|
+
export const DecisionSchema = z.object({ decisionId: z.string().min(1), approve: z.boolean(), feedback: z.string().optional() });
|
|
207
|
+
export const AnswerSchema = z.object({
|
|
208
|
+
requestId: z.string().min(1),
|
|
209
|
+
answers: z.record(z.string(), z.array(z.string())).optional(),
|
|
210
|
+
cancelled: z.boolean().optional(),
|
|
211
|
+
});
|
|
212
|
+
// Steering: a user message delivered INTO the running turn (injected between tool calls, Claude Code style),
|
|
213
|
+
// keyed by the conversation whose turn is in flight. NOT_FOUND when no steerable turn is running — the client
|
|
214
|
+
// then falls back to a fresh send.
|
|
215
|
+
export const SteerSchema = z.object({ conversationId: z.string().min(1), text: z.string().min(1).max(20_000) });
|
|
216
|
+
// True cancel for the conversation's in-flight turn — aborts the agent daemon-side, unlike closing the
|
|
217
|
+
// /agent fetch (which sends no cancel frame).
|
|
218
|
+
export const StopTurnSchema = z.object({ conversationId: z.string().min(1) });
|
|
219
|
+
|
|
220
|
+
// ---- provider oauth ----
|
|
221
|
+
// Claude uses the PKCE authorize-URL + paste-back handshake (start → exchange). Codex uses OpenAI's device-code
|
|
222
|
+
// flow (start → poll): the browser signs in at verificationUri and enters userCode; the daemon polls until done.
|
|
223
|
+
// A sandbox can hold several accounts per provider side by side: `id` is the daemon-minted store key, `label`
|
|
224
|
+
// the user's display name (auto-filled from the sign-in identity where the token carries one). Tokens never
|
|
225
|
+
// ride this shape — connection status is existence in the list.
|
|
226
|
+
|
|
227
|
+
export const OauthAccountSchema = z.object({
|
|
228
|
+
id: z.string(),
|
|
229
|
+
label: z.string(),
|
|
230
|
+
scope: z.string().optional(),
|
|
231
|
+
connectedAt: z.number(), // epoch ms
|
|
232
|
+
// Set only when the account's stored credential can no longer be refreshed (revoked/expired refresh token)
|
|
233
|
+
// — the user must reconnect. Absent ⇒ healthy or not-yet-probed; `detail` carries the reason for the UI.
|
|
234
|
+
// Provider-agnostic; only Codex probes it today (Claude refreshes on-demand, Grok's tokens are OpenCode's).
|
|
235
|
+
needsReauth: z.boolean().optional(),
|
|
236
|
+
detail: z.string().optional(),
|
|
237
|
+
});
|
|
238
|
+
export type OauthAccount = z.infer<typeof OauthAccountSchema>;
|
|
239
|
+
export const OauthAccountListSchema = z.object({ accounts: z.array(OauthAccountSchema) });
|
|
240
|
+
// Address one account of a provider (disconnect, and the turn's `account`).
|
|
241
|
+
export const AccountIdSchema = z.object({ id: z.string().min(1) });
|
|
242
|
+
// The completing calls carry the user-chosen label (blank ⇒ the daemon derives one from the sign-in identity
|
|
243
|
+
// or a provider default).
|
|
244
|
+
export const OauthExchangeSchema = z.object({
|
|
245
|
+
code: z.string().min(1),
|
|
246
|
+
verifier: z.string().min(1),
|
|
247
|
+
state: z.string().min(1),
|
|
248
|
+
label: z.string().optional(),
|
|
249
|
+
});
|
|
250
|
+
export const AuthorizeChallengeSchema = z.object({ authorizeUrl: z.string(), verifier: z.string(), state: z.string() });
|
|
251
|
+
export const CodexDeviceStartSchema = z.object({
|
|
252
|
+
userCode: z.string(),
|
|
253
|
+
deviceAuthId: z.string(),
|
|
254
|
+
interval: z.number(),
|
|
255
|
+
verificationUri: z.string(),
|
|
256
|
+
});
|
|
257
|
+
export const CodexDevicePollSchema = z.object({ deviceAuthId: z.string().min(1), userCode: z.string().min(1), label: z.string().optional() });
|
|
258
|
+
// The poll answer: `pending` while the user is still signing in; the created account once done.
|
|
259
|
+
export const CodexPollResultSchema = z.object({ pending: z.boolean(), account: OauthAccountSchema.optional() });
|
|
260
|
+
// xAI Grok (via OpenCode) uses subscription OAuth via the headless device-code method. `start` returns the
|
|
261
|
+
// `url` the user opens (xAI's verification_uri_complete, which pre-fills the code) and `code` — the same
|
|
262
|
+
// one-time code, surfaced so the card matches x.ai exactly. There is no paste-back: OpenCode polls to
|
|
263
|
+
// completion and the UI polls `/grok/accounts`.
|
|
264
|
+
// ponytail: OpenCode holds one xAI auth per data dir, so Grok stays single-account — the list is 0 or 1. Per
|
|
265
|
+
// account would need an OpenCode server per data dir; add when there's demand.
|
|
266
|
+
// A device-code login start: the verification URL + the one-time code the user enters there. Shared by the
|
|
267
|
+
// native Grok flow (via OpenCode) and the routed-provider subscription connect (codex/grok via CLIProxyAPI).
|
|
268
|
+
export const DeviceStartSchema = z.object({ url: z.string(), code: z.string() });
|
|
269
|
+
// A provider's model catalog, resolved daemon-side from live discovery with a persisted last-known-good list and
|
|
270
|
+
// a seed floor (Grok via opencode.ts xaiModels, Codex via codex-models.ts, Claude via the Agent SDK's
|
|
271
|
+
// supportedModels) — never empty, so the picker is never blank. `label` is the humanized display name; `default`
|
|
272
|
+
// is the model a fresh chat on that provider seeds (always present). Shared by /grok/models, /codex/models,
|
|
273
|
+
// /claude/models. `efforts` is the reasoning-effort tiers the model accepts (Claude reports them per model);
|
|
274
|
+
// empty ⇒ the client's default tiers.
|
|
275
|
+
export const ModelSchema = z.object({ id: z.string(), label: z.string(), efforts: z.array(z.string()).optional() });
|
|
276
|
+
export const ModelsSchema = z.object({ models: z.array(ModelSchema), default: z.string() });
|
|
277
|
+
|
|
278
|
+
// ---- sessions ----
|
|
279
|
+
|
|
280
|
+
export const SessionIdParamSchema = z.object({ id: z.string() });
|
|
281
|
+
export const SessionSummarySchema = z.object({ id: z.string(), title: z.string(), updatedAt: z.number() });
|
|
282
|
+
export const SessionsListSchema = z.object({ sessions: z.array(SessionSummarySchema) });
|
|
283
|
+
export const SessionTranscriptSchema = z.object({ messages: z.array(SessionTranscriptMessageSchema) });
|
|
284
|
+
|
|
285
|
+
// ---- settings: per-sandbox agent settings (.intentic/settings.json) ----
|
|
286
|
+
// Small user-owned config the /settings routes edit and streamAgent reads — all opt-in booleans the owner
|
|
287
|
+
// toggles in the UI (so each can be A/B benchmarked):
|
|
288
|
+
// stableSystemPrompt — keeps the system prompt byte-stable across turns (the delegation note rides the user
|
|
289
|
+
// message instead of the preset `append`) so the provider prompt cache survives.
|
|
290
|
+
// skills — names of baked-tool skills to load into .claude/skills so the agent reaches for them
|
|
291
|
+
// (e.g. "lsp" — TS rename + diagnostics over the language service); a name absent ⇒ its
|
|
292
|
+
// skill file isn't written, so the agent doesn't reach for it. Data-driven: a new baked
|
|
293
|
+
// tool is one daemon-side registry entry, not a new settings field.
|
|
294
|
+
// hashlineEdits — swaps the native Read/Edit/Write for hash-anchored edits on the Claude path (stale-file
|
|
295
|
+
// guard + fewer output tokens); off ⇒ the native file tools.
|
|
296
|
+
// terseOutput — appends a concise-response steer to the end of the system prompt (a stable suffix, so it
|
|
297
|
+
// composes with stableSystemPrompt) to cut the model's OWN output tokens.
|
|
298
|
+
// iqSearch — loads the image-baked iq Claude Code plugin (skill + SessionStart nudge) so the agent
|
|
299
|
+
// prefers the iq CLI over grep/find/Glob; off ⇒ plugin not loaded, native search tools
|
|
300
|
+
// only. Opt-in (default off); the browser Search box uses iq regardless.
|
|
301
|
+
// outputCleaners — the Bash output-cleaner spec (agent-output-filter): "off" = filter disabled (default),
|
|
302
|
+
// "" = all cleaners on, else an iq-style allow-list / default-minus
|
|
303
|
+
// spec ("git,pnpm" = only those; "-cap" = all except). Threaded to the filter via env.
|
|
304
|
+
// outputHoldout — measurement control: a fraction [0,1] of Bash commands whose output bypasses cleaning
|
|
305
|
+
// (recorded raw as `heldOut`), so the savings report compares a real cleaned-vs-raw
|
|
306
|
+
// population instead of an estimate. 0 = no holdout (default).
|
|
307
|
+
// filterBackend — which cleaner runs the compression: "native" (agent-output-filter, default) or "rtk"
|
|
308
|
+
// (the rtk binary from its installed extension, rewritten at the PreToolUse hook) — an
|
|
309
|
+
// A/B backend switch, so native and rtk can be benchmarked head-to-head.
|
|
310
|
+
// The booleans default off, skills defaults [] (no skill loaded), outputCleaners defaults "off" (cleaning off),
|
|
311
|
+
// outputHoldout 0, filterBackend "native" — a fresh sandbox starts with cleaning and iq off until the owner enables them.
|
|
312
|
+
|
|
313
|
+
export const SandboxSettingsSchema = z.object({
|
|
314
|
+
stableSystemPrompt: z.boolean(),
|
|
315
|
+
skills: z.array(z.string()),
|
|
316
|
+
hashlineEdits: z.boolean(),
|
|
317
|
+
terseOutput: z.boolean(),
|
|
318
|
+
iqSearch: z.boolean(),
|
|
319
|
+
outputCleaners: z.string(),
|
|
320
|
+
outputHoldout: z.number().min(0).max(1),
|
|
321
|
+
filterBackend: z.enum(["native", "rtk"]),
|
|
322
|
+
});
|
|
323
|
+
export type SandboxSettings = z.infer<typeof SandboxSettingsSchema>;
|
|
324
|
+
|
|
325
|
+
// ---- output-cleaner savings report (rtk-`gain`-style) ----
|
|
326
|
+
// Aggregated from historyRoot/logs/filter-stats.jsonl (one row per agent Bash command). `perCleaner` attributes
|
|
327
|
+
// which cleaner ids fired across commands; `holdout` is the measured control (commands the holdout bypassed) vs
|
|
328
|
+
// the cleaned population — a real saved-% rather than an estimate; `gaps` are high-volume commands that matched
|
|
329
|
+
// no cleaner (the next handler to write). Empty/zeroed when no commands have run yet.
|
|
330
|
+
export const CleanerSavingsSchema = z.object({
|
|
331
|
+
commands: z.number(),
|
|
332
|
+
rawTokens: z.number(),
|
|
333
|
+
emittedTokens: z.number(),
|
|
334
|
+
savedPct: z.number(),
|
|
335
|
+
perCleaner: z.array(z.object({ id: z.string(), commands: z.number() })),
|
|
336
|
+
holdout: z.object({ cleaned: z.number(), heldOut: z.number(), measuredSavedPct: z.number().optional() }),
|
|
337
|
+
gaps: z.array(z.object({ command: z.string(), tokens: z.number() })),
|
|
338
|
+
});
|
|
339
|
+
export type CleanerSavings = z.infer<typeof CleanerSavingsSchema>;
|
|
340
|
+
|
|
341
|
+
// ---- intentic CLI ----
|
|
342
|
+
|
|
343
|
+
export const IntenticRunSchema = z.object({ args: z.array(z.string()) });
|
|
344
|
+
|
|
345
|
+
// ---- git ----
|
|
346
|
+
|
|
347
|
+
export const CommitSchema = RepoParamSchema.extend({
|
|
348
|
+
message: z.string().min(1),
|
|
349
|
+
// Repo-relative paths to commit; absent ⇒ commit everything changed in the repo.
|
|
350
|
+
paths: z.array(z.string().min(1)).max(500).optional(),
|
|
351
|
+
});
|
|
352
|
+
export const DiscardSchema = RepoParamSchema.extend({
|
|
353
|
+
// Repo-relative paths to discard; absent ⇒ discard every uncommitted change in the repo.
|
|
354
|
+
paths: z.array(z.string().min(1)).max(500).optional(),
|
|
355
|
+
});
|
|
356
|
+
export const PushSchema = RepoParamSchema.extend({ branch: z.string().min(1) });
|
|
357
|
+
export const GitFileQuerySchema = RepoParamSchema.extend({ path: z.string().min(1) });
|
|
358
|
+
export const GitFileWriteSchema = RepoParamSchema.extend({ path: z.string().min(1), content: z.string() });
|
|
359
|
+
export const GitFileDiffQuerySchema = RepoParamSchema.extend({ path: z.string().min(1) });
|
|
360
|
+
export const GitStatusSchema = z.object({ branch: z.string(), dirty: z.boolean(), files: z.array(z.string()) });
|
|
361
|
+
export const GitFilesSchema = z.object({ files: z.array(z.string()) });
|
|
362
|
+
export const GitFileSchema = z.object({ path: z.string(), content: z.string() });
|
|
363
|
+
export const CommitResultSchema = z.object({ committed: z.boolean() });
|
|
364
|
+
// One change to a file — an uncommitted working-tree change (status vs HEAD, untracked included), an agent
|
|
365
|
+
// worktree's delta vs its base, or a file in a commit. `additions`/`deletions` are the numstat line counts,
|
|
366
|
+
// undefined for a binary file (git reports "-"/"-") or an untracked file (no HEAD blob to diff against).
|
|
367
|
+
export const GitChangeSchema = z.object({
|
|
368
|
+
// Repo-relative path with forward slashes; for "renamed" the NEW path (`from` carries the old one).
|
|
369
|
+
path: z.string(),
|
|
370
|
+
status: z.enum(["added", "modified", "deleted", "renamed", "type-changed"]),
|
|
371
|
+
from: z.string().optional(),
|
|
372
|
+
additions: z.number().optional(),
|
|
373
|
+
deletions: z.number().optional(),
|
|
374
|
+
});
|
|
375
|
+
export type GitChange = z.infer<typeof GitChangeSchema>;
|
|
376
|
+
export const RepoChangesSchema = z.object({
|
|
377
|
+
// The {repo} param the per-repo git routes accept: "root" or a repo id (its root-relative dir).
|
|
378
|
+
repo: z.string(),
|
|
379
|
+
// Absent on an unborn HEAD (a repo initialized but never committed).
|
|
380
|
+
branch: z.string().optional(),
|
|
381
|
+
changes: z.array(GitChangeSchema),
|
|
382
|
+
});
|
|
383
|
+
export type RepoChanges = z.infer<typeof RepoChangesSchema>;
|
|
384
|
+
// The aggregated review set across every repo (root + every discovered repo); only repos with changes appear.
|
|
385
|
+
export const GitChangesSchema = z.object({ repos: z.array(RepoChangesSchema) });
|
|
386
|
+
export type GitChanges = z.infer<typeof GitChangesSchema>;
|
|
387
|
+
|
|
388
|
+
// ---- git history graph (the "Git Graph" view over a repo's real commits) ----
|
|
389
|
+
// A hex sha (full or git-abbreviated): the only shape the graph ever sends back, so the per-commit routes
|
|
390
|
+
// constrain to it rather than accepting an arbitrary git revision expression.
|
|
391
|
+
const ShaSchema = z.string().regex(/^[0-9a-f]{4,64}$/);
|
|
392
|
+
// One commit in the graph. `parents` (0 = root, 1 = normal, 2+ = merge) drive the lane layout, computed
|
|
393
|
+
// client-side. `refs` are the branch/tag decorations at this commit (tags keep their `tag: ` prefix; the bare
|
|
394
|
+
// "HEAD" marker is lifted into `head` instead). `at` is author time in ms since epoch; `short` is git's
|
|
395
|
+
// abbreviated sha; `body` is the message minus its subject line.
|
|
396
|
+
export const GitCommitSchema = z.object({
|
|
397
|
+
sha: z.string(),
|
|
398
|
+
short: z.string(),
|
|
399
|
+
parents: z.array(z.string()),
|
|
400
|
+
subject: z.string(),
|
|
401
|
+
body: z.string(),
|
|
402
|
+
author: z.string(),
|
|
403
|
+
email: z.string(),
|
|
404
|
+
at: z.number(),
|
|
405
|
+
refs: z.array(z.string()),
|
|
406
|
+
head: z.boolean(),
|
|
407
|
+
});
|
|
408
|
+
export type GitCommit = z.infer<typeof GitCommitSchema>;
|
|
409
|
+
// One repo's log: commits newest-first across ALL refs (branch topology is the point of a graph), plus the
|
|
410
|
+
// checked-out branch (absent on a detached HEAD or an unborn repo).
|
|
411
|
+
export const GitLogSchema = z.object({ repo: z.string(), branch: z.string().optional(), commits: z.array(GitCommitSchema) });
|
|
412
|
+
export type GitLog = z.infer<typeof GitLogSchema>;
|
|
413
|
+
export const GitLogQuerySchema = RepoParamSchema.extend({ limit: z.coerce.number().int().positive().max(2000).optional() });
|
|
414
|
+
// Every real git repo under /work as root-relative dir ids ("root" is implicit — the /work repo itself).
|
|
415
|
+
export const GitReposSchema = z.object({ repos: z.array(z.string()) });
|
|
416
|
+
export type GitRepos = z.infer<typeof GitReposSchema>;
|
|
417
|
+
export const GitCommitDiffQuerySchema = RepoParamSchema.extend({ sha: ShaSchema });
|
|
418
|
+
// A commit's changed files (vs its first parent; a root commit vs the empty tree) — the graph's detail tree
|
|
419
|
+
// renders these (line stats included) and reuses the diff UI on click. Just GitChanges: the line stats live on
|
|
420
|
+
// GitChange now, so working-tree and commit files share one shape.
|
|
421
|
+
export const GitCommitDiffSchema = z.object({ files: z.array(GitChangeSchema) });
|
|
422
|
+
export type GitCommitDiff = z.infer<typeof GitCommitDiffSchema>;
|
|
423
|
+
export const GitCommitFileDiffQuerySchema = RepoParamSchema.extend({ sha: ShaSchema, path: z.string().min(1) });
|
|
424
|
+
// Git write actions from the graph's commit context menu (VSCode "Git Graph" parity). Non-destructive: branch
|
|
425
|
+
// and tag just add a ref (HEAD + worktree untouched, no checkpoint). Sequence ops (revert / cherry-pick /
|
|
426
|
+
// merge / rebase / drop) add or replay commits and are auto-checkpointed daemon-side; a conflict aborts and
|
|
427
|
+
// reports `ok:false` (an expected outcome, not a throw). Checkout and reset move HEAD (reset --hard discards
|
|
428
|
+
// the worktree) — also auto-checkpointed. A `{repo, sha}` names the target commit for every commit-scoped
|
|
429
|
+
// action; a ref name (branch/tag) is validated structurally, git enforces the rest of ref-name legality.
|
|
430
|
+
const RefNameSchema = z
|
|
431
|
+
.string()
|
|
432
|
+
.regex(/^[A-Za-z0-9][A-Za-z0-9._/-]*$/)
|
|
433
|
+
.max(200);
|
|
434
|
+
export const GitBranchCreateSchema = RepoParamSchema.extend({ sha: ShaSchema, name: RefNameSchema });
|
|
435
|
+
export const GitTagCreateSchema = RepoParamSchema.extend({ sha: ShaSchema, name: RefNameSchema });
|
|
436
|
+
export const GitCheckoutSchema = RepoParamSchema.extend({ ref: RefNameSchema });
|
|
437
|
+
export const GitResetSchema = RepoParamSchema.extend({ sha: ShaSchema, mode: z.enum(["soft", "mixed", "hard"]) });
|
|
438
|
+
export const GitCommitActionSchema = RepoParamSchema.extend({ sha: ShaSchema });
|
|
439
|
+
export const GitActionResultSchema = z.object({ ok: z.boolean(), reason: z.string().optional() });
|
|
440
|
+
export type GitActionResult = z.infer<typeof GitActionResultSchema>;
|
|
441
|
+
|
|
442
|
+
// ---- history: daemon-owned workspace snapshots (diff + restore) ----
|
|
443
|
+
// The daemon snapshots /work into bare git dirs on /history (outside the agent's reach). A "snapshot" groups
|
|
444
|
+
// one commit per scope (root + each nested repo) under a shared id. Only checkpoint triggers (turn / user /
|
|
445
|
+
// pre-restore / restore) are listed; "interval" captures are a hidden safety net that dissolves into the next
|
|
446
|
+
// visible checkpoint's diff.
|
|
447
|
+
|
|
448
|
+
export const SnapshotTriggerSchema = z.enum(["turn", "interval", "pre-restore", "restore", "user"]);
|
|
449
|
+
export type SnapshotTrigger = z.infer<typeof SnapshotTriggerSchema>;
|
|
450
|
+
export const SnapshotSchema = z.object({
|
|
451
|
+
id: z.string(),
|
|
452
|
+
// Committer time, ms since epoch.
|
|
453
|
+
at: z.number(),
|
|
454
|
+
trigger: SnapshotTriggerSchema,
|
|
455
|
+
// Human-readable checkpoint label — the turn's prompt for "turn" snapshots; absent otherwise.
|
|
456
|
+
label: z.string().optional(),
|
|
457
|
+
});
|
|
458
|
+
export type Snapshot = z.infer<typeof SnapshotSchema>;
|
|
459
|
+
export const SnapshotsListSchema = z.object({ snapshots: z.array(SnapshotSchema) });
|
|
460
|
+
export const SnapshotIdSchema = z.object({ id: z.string().min(1) });
|
|
461
|
+
export const SnapshotChangeSchema = z.object({
|
|
462
|
+
scope: z.string(),
|
|
463
|
+
// Scope-relative path with forward slashes.
|
|
464
|
+
path: z.string(),
|
|
465
|
+
status: z.enum(["added", "modified", "deleted", "type-changed"]),
|
|
466
|
+
});
|
|
467
|
+
export type SnapshotChange = z.infer<typeof SnapshotChangeSchema>;
|
|
468
|
+
export const SnapshotDiffSchema = z.object({ changes: z.array(SnapshotChangeSchema) });
|
|
469
|
+
export const SnapshotFileDiffQuerySchema = z.object({
|
|
470
|
+
id: z.string().min(1),
|
|
471
|
+
scope: z.string().min(1),
|
|
472
|
+
path: z.string().min(1),
|
|
473
|
+
});
|
|
474
|
+
// Both sides of a file diff — a snapshot vs its parent, or a working tree vs HEAD; an absent side means the
|
|
475
|
+
// file was added/deleted. Binary or oversized content is flagged instead of shipped.
|
|
476
|
+
export const FileDiffSchema = z.object({
|
|
477
|
+
before: z.string().optional(),
|
|
478
|
+
after: z.string().optional(),
|
|
479
|
+
binary: z.boolean().optional(),
|
|
480
|
+
truncated: z.boolean().optional(),
|
|
481
|
+
});
|
|
482
|
+
export type FileDiff = z.infer<typeof FileDiffSchema>;
|
|
483
|
+
|
|
484
|
+
// ---- workspace tree + files ----
|
|
485
|
+
|
|
486
|
+
// One node of the full /work filesystem tree the agent sees (untracked + generated files included), distinct
|
|
487
|
+
// from the git-tracked listing. `path` is root-relative with forward slashes so it feeds straight back to the
|
|
488
|
+
// file route. Recursive via zod's getter form (so the type is inferred, not hand-annotated).
|
|
489
|
+
export const WorkspaceTreeEntrySchema = z.object({
|
|
490
|
+
name: z.string(),
|
|
491
|
+
path: z.string(),
|
|
492
|
+
type: z.enum(["file", "dir"]),
|
|
493
|
+
size: z.number().optional(),
|
|
494
|
+
// Set on a dir whose child list was cut short by the entry cap — some of its items aren't in `children`.
|
|
495
|
+
truncated: z.boolean().optional(),
|
|
496
|
+
// Ignored-by-tooling (node_modules, .git, .gitignore'd paths, browser profiles): the client grays the row.
|
|
497
|
+
// An ignored DIR is listed without `children` — the client lazy-loads it via /workspace/children on expand.
|
|
498
|
+
ignored: z.boolean().optional(),
|
|
499
|
+
get children() {
|
|
500
|
+
return z.array(WorkspaceTreeEntrySchema).optional();
|
|
501
|
+
},
|
|
502
|
+
});
|
|
503
|
+
export type WorkspaceTreeEntry = z.infer<typeof WorkspaceTreeEntrySchema>;
|
|
504
|
+
export const WorkspaceTreeSchema = z.object({
|
|
505
|
+
root: z.string(),
|
|
506
|
+
tree: z.array(WorkspaceTreeEntrySchema),
|
|
507
|
+
// True when the root's own entries were cut by the entry cap (per-dir cuts are flagged on each dir entry).
|
|
508
|
+
truncated: z.boolean(),
|
|
509
|
+
});
|
|
510
|
+
export type WorkspaceTree = z.infer<typeof WorkspaceTreeSchema>;
|
|
511
|
+
// Lazy-load one directory's children — for an ignored dir the tree walk didn't descend into. Every returned entry
|
|
512
|
+
// is itself `ignored` (it lives under an ignored subtree); child dirs again carry no `children`, so they lazy-load
|
|
513
|
+
// on their own expand. `truncated` ⇒ the dir's child list was cut by the entry cap.
|
|
514
|
+
export const WorkspaceChildrenQuerySchema = z.object({ path: z.string().min(1) });
|
|
515
|
+
export const WorkspaceChildrenSchema = z.object({
|
|
516
|
+
entries: z.array(WorkspaceTreeEntrySchema),
|
|
517
|
+
truncated: z.boolean(),
|
|
518
|
+
});
|
|
519
|
+
export type WorkspaceChildren = z.infer<typeof WorkspaceChildrenSchema>;
|
|
520
|
+
export const WorkspaceFileQuerySchema = z.object({ path: z.string().min(1) });
|
|
521
|
+
export const WorkspaceFileSchema = z.object({ path: z.string(), content: z.string() });
|
|
522
|
+
// Direct file management over the /work tree (delete / new folder / rename+move / copy). Byte writes + the
|
|
523
|
+
// editor's text save go through the plain POST /workspace/upload route (a body doesn't fit oRPC), not here.
|
|
524
|
+
export const WorkspaceDirSchema = z.object({ path: z.string().min(1) });
|
|
525
|
+
export const WorkspaceMoveSchema = z.object({ from: z.string().min(1), to: z.string().min(1) });
|
|
526
|
+
// Deterministic (no-LLM) classification of the dropped workspace: each repo dir and loose file sorted into one
|
|
527
|
+
// coarse bucket. Read-only — the browser turns it into a proposed layout and applies the accepted moves via the
|
|
528
|
+
// existing /workspace/move route. `reason` records the winning signal (magic:<mime>, ext:<ext>,
|
|
529
|
+
// repository:<marker>, text-content, unknown) so the proposal is explainable.
|
|
530
|
+
export const WorkspaceBucketSchema = z.enum(["repositories", "documents", "media", "archives", "other"]);
|
|
531
|
+
export type WorkspaceBucket = z.infer<typeof WorkspaceBucketSchema>;
|
|
532
|
+
export const WorkspaceClassificationSchema = z.object({
|
|
533
|
+
classifications: z.array(z.object({ path: z.string(), bucket: WorkspaceBucketSchema, reason: z.string() })),
|
|
534
|
+
});
|
|
535
|
+
export type WorkspaceClassification = z.infer<typeof WorkspaceClassificationSchema>;
|
|
536
|
+
// ---- workspace search ----
|
|
537
|
+
|
|
538
|
+
// The workspace-search wire shape — shared by the daemon's /workspace/search route and the web client.
|
|
539
|
+
// (Implementation detail, not part of the contract: the daemon backs this route with a resident in-process iq
|
|
540
|
+
// engine; the engine is interchangeable behind this shape.) Groups are relevance-ranked (best first, never path
|
|
541
|
+
// order); each hit carries the match-reason tags the fused engines contributed. `start`/`end` are char offsets
|
|
542
|
+
// within `text` so clients highlight without re-finding the needle.
|
|
543
|
+
export const WorkspaceSearchQuerySchema = z.object({
|
|
544
|
+
query: z.string().min(2).max(512),
|
|
545
|
+
// Search verbs only — anchor/git verbs (outline, context, log, who, …) are CLI-only surface.
|
|
546
|
+
mode: z.enum(["q", "find", "files", "def", "refs", "sym", "ast", "ask"]).optional(),
|
|
547
|
+
includeIgnored: z.stringbool().optional(),
|
|
548
|
+
limit: z.coerce.number().int().positive().optional(),
|
|
549
|
+
after: z.string().optional(),
|
|
550
|
+
});
|
|
551
|
+
export const WorkspaceSearchTagSchema = z.object({
|
|
552
|
+
kind: z.enum(["def", "text", "sem", "bm25", "rerank", "path", "import", "call", "type", "write", "fuzzy", "heuristic"]),
|
|
553
|
+
score: z.number().optional(),
|
|
554
|
+
});
|
|
555
|
+
export type WorkspaceSearchTag = z.infer<typeof WorkspaceSearchTagSchema>;
|
|
556
|
+
export const WorkspaceSearchHitSchema = z.object({
|
|
557
|
+
line: z.number(),
|
|
558
|
+
text: z.string(),
|
|
559
|
+
start: z.number().optional(),
|
|
560
|
+
end: z.number().optional(),
|
|
561
|
+
tags: z.array(WorkspaceSearchTagSchema),
|
|
562
|
+
// Enclosing symbol ("createWidget (fn)") — parent-document context so the reader often needs no follow-up.
|
|
563
|
+
context: z.string().optional(),
|
|
564
|
+
});
|
|
565
|
+
export type WorkspaceSearchHit = z.infer<typeof WorkspaceSearchHitSchema>;
|
|
566
|
+
export const WorkspaceSearchGroupSchema = z.object({ path: z.string(), score: z.number(), hits: z.array(WorkspaceSearchHitSchema) });
|
|
567
|
+
export type WorkspaceSearchGroup = z.infer<typeof WorkspaceSearchGroupSchema>;
|
|
568
|
+
// `building` = index still filling (progress 0..1, e.g. embeddings pending); `stale` = revalidation was skipped
|
|
569
|
+
// (cursor replay). ageMs = time since the index last matched the disk state.
|
|
570
|
+
export const WorkspaceSearchFreshnessSchema = z.object({
|
|
571
|
+
state: z.enum(["fresh", "building", "stale"]),
|
|
572
|
+
ageMs: z.number().optional(),
|
|
573
|
+
progress: z.number().optional(),
|
|
574
|
+
});
|
|
575
|
+
export type WorkspaceSearchFreshness = z.infer<typeof WorkspaceSearchFreshnessSchema>;
|
|
576
|
+
export const WorkspaceSearchResultSchema = z.object({
|
|
577
|
+
mode: z.string(),
|
|
578
|
+
total: z.number(),
|
|
579
|
+
shown: z.number(),
|
|
580
|
+
groups: z.array(WorkspaceSearchGroupSchema),
|
|
581
|
+
freshness: WorkspaceSearchFreshnessSchema,
|
|
582
|
+
truncated: z.boolean(),
|
|
583
|
+
cursor: z.string().optional(),
|
|
584
|
+
hint: z.string().optional(),
|
|
585
|
+
// Code-graph neighbors of the top hits (definition anchors + ready-made follow-up commands).
|
|
586
|
+
related: z.array(z.string()).optional(),
|
|
587
|
+
// Run provenance for benchmarking: retrieval stages DISABLED this invocation (absent = full pipeline).
|
|
588
|
+
features: z.array(z.string()).optional(),
|
|
589
|
+
});
|
|
590
|
+
export type WorkspaceSearchResult = z.infer<typeof WorkspaceSearchResultSchema>;
|
|
591
|
+
|
|
592
|
+
// ---- workspace repos ----
|
|
593
|
+
|
|
594
|
+
// Every discovered repo's id (root-relative dir under /work), sorted — roles included.
|
|
595
|
+
export const ReposListSchema = z.object({ repos: z.array(z.string()) });
|
|
596
|
+
export const CloneRepoSchema = z.object({ name: z.string().min(1), cloneUrl: z.string().min(1), branch: z.string().optional() });
|
|
597
|
+
export const CloneResultSchema = z.object({ name: z.string(), path: z.string() });
|
|
598
|
+
// Per-repo result of a workspace sync (fetch + guarded fast-forward). `status` mirrors GitSyncResult plus the
|
|
599
|
+
// turn-orchestration outcomes skipped/error; behind/ahead/head/message are present per status (see RepoSyncOutcome).
|
|
600
|
+
export const RepoSyncSchema = z.object({
|
|
601
|
+
repo: z.string(),
|
|
602
|
+
status: z.enum(["updated", "current", "dirty", "diverged", "no-remote", "skipped", "error"]),
|
|
603
|
+
behind: z.number().optional(),
|
|
604
|
+
ahead: z.number().optional(),
|
|
605
|
+
head: z.string().optional(),
|
|
606
|
+
message: z.string().optional(),
|
|
607
|
+
});
|
|
608
|
+
export const WorkspaceSyncSchema = z.object({ repos: z.array(RepoSyncSchema) });
|
|
609
|
+
// Add one or more named app instances into an EXISTING monorepo. Each entry pairs a template key from the
|
|
610
|
+
// source repo's templates.json manifest (e.g. "api", "web", "landing") with a user-chosen instance name
|
|
611
|
+
// (e.g. "shop-api"); {repo} names the target monorepo.
|
|
612
|
+
export const AppInstanceInputSchema = z.object({
|
|
613
|
+
template: z.string().min(1),
|
|
614
|
+
name: z
|
|
615
|
+
.string()
|
|
616
|
+
.min(1)
|
|
617
|
+
.regex(/^[a-z][a-z0-9-]*$/),
|
|
618
|
+
});
|
|
619
|
+
export type AppInstanceInput = z.infer<typeof AppInstanceInputSchema>;
|
|
620
|
+
export const AddAppsSchema = z.object({
|
|
621
|
+
repo: z.string(),
|
|
622
|
+
apps: z.array(AppInstanceInputSchema).min(1),
|
|
623
|
+
});
|
|
624
|
+
|
|
625
|
+
// Run vitest for one or more repo-relative project dirs in a named one-shot tmux panel session
|
|
626
|
+
// (panel-<repo>--<session>), driven by the apps extension's Run-tests actions. `session` is a slug suffix
|
|
627
|
+
// (an app/package name as `<name>__test`, or `tests` for the library section); `dirs` are repo-relative
|
|
628
|
+
// package dirs, where "" targets the repo root.
|
|
629
|
+
export const RunTestsSchema = z.object({
|
|
630
|
+
repo: z.string(),
|
|
631
|
+
session: z.string(),
|
|
632
|
+
dirs: z.array(z.string()).min(1),
|
|
633
|
+
});
|
|
634
|
+
|
|
635
|
+
// One addable app type the configured source repo offers (from its templates.json), listed for the operator
|
|
636
|
+
// panel's Add-app picker: the manifest key + its label/description.
|
|
637
|
+
export const TemplateSummarySchema = z.object({ key: z.string(), label: z.string(), description: z.string() });
|
|
638
|
+
export type TemplateSummary = z.infer<typeof TemplateSummarySchema>;
|
|
639
|
+
export const TemplatesListSchema = z.object({ templates: z.array(TemplateSummarySchema) });
|
|
640
|
+
export type TemplatesList = z.infer<typeof TemplatesListSchema>;
|
|
641
|
+
|
|
642
|
+
// One app instance currently in a monorepo, with its own preview dev server + live status (started/stopped
|
|
643
|
+
// from the apps extension). `app` is the user-chosen instance name (the _apps/ dir); `template` is the
|
|
644
|
+
// manifest key it was created from (api/web/landing). previewUrl is
|
|
645
|
+
// https://preview-<repo>--<app>-<sandboxId>.<zone> (absent on loopback — no zone or no connect token).
|
|
646
|
+
export const RepoAppSchema = z.object({
|
|
647
|
+
app: z.string(),
|
|
648
|
+
template: z.string(),
|
|
649
|
+
previewUrl: z.string().optional(),
|
|
650
|
+
running: z.boolean(),
|
|
651
|
+
healthy: z.boolean(),
|
|
652
|
+
});
|
|
653
|
+
export type RepoApp = z.infer<typeof RepoAppSchema>;
|
|
654
|
+
export const AppsListSchema = z.object({ apps: z.array(RepoAppSchema) });
|
|
655
|
+
export type AppsList = z.infer<typeof AppsListSchema>;
|
|
656
|
+
// One workspace package in a pnpm monorepo, discovered from pnpm-workspace.yaml's packages globs. `dir` is the
|
|
657
|
+
// repo-relative package dir (e.g. "_apps/web"); `group` is its top-level dir segment (e.g. "_apps"), the
|
|
658
|
+
// dependencies view's coloring axis.
|
|
659
|
+
export const WorkspacePackageSchema = z.object({ name: z.string(), dir: z.string(), group: z.string() });
|
|
660
|
+
export type WorkspacePackage = z.infer<typeof WorkspacePackageSchema>;
|
|
661
|
+
export const WorkspaceDepTypeSchema = z.enum(["prod", "dev", "peer"]);
|
|
662
|
+
export type WorkspaceDepType = z.infer<typeof WorkspaceDepTypeSchema>;
|
|
663
|
+
// A workspace-internal dependency edge: `from` DEPENDS ON `to` (from's package.json lists to), typed by which
|
|
664
|
+
// dependency block declared it. Pure data — layout/direction is the client's concern.
|
|
665
|
+
export const WorkspaceDepEdgeSchema = z.object({ from: z.string(), to: z.string(), type: WorkspaceDepTypeSchema });
|
|
666
|
+
export type WorkspaceDepEdge = z.infer<typeof WorkspaceDepEdgeSchema>;
|
|
667
|
+
export const WorkspaceGraphSchema = z.object({ packages: z.array(WorkspacePackageSchema), edges: z.array(WorkspaceDepEdgeSchema) });
|
|
668
|
+
export type WorkspaceGraph = z.infer<typeof WorkspaceGraphSchema>;
|
|
669
|
+
// Path params for the per-repo apps routes: the monorepo name (validated in the handler like PanelRepoParam)
|
|
670
|
+
// and, for per-app preview control (start/stop), the app key (api/web/landing).
|
|
671
|
+
export const RepoAppsParamSchema = z.object({ repo: z.string() });
|
|
672
|
+
export const AppParamSchema = z.object({
|
|
673
|
+
repo: z.string(),
|
|
674
|
+
app: z
|
|
675
|
+
.string()
|
|
676
|
+
.min(1)
|
|
677
|
+
.regex(/^[a-z][a-z0-9-]*$/),
|
|
678
|
+
});
|
|
679
|
+
|
|
680
|
+
// ---- inventory: the i.have.* / i.want.service entries in deploy.config.ts's managed region ----
|
|
681
|
+
// The daemon renders/parses these; the browser edits them through the inventory routes. Moved here from the
|
|
682
|
+
// daemon's deploy-config.ts so the daemon and the browser validate against ONE schema (no cross-repo dupes).
|
|
683
|
+
|
|
684
|
+
export const InventoryProviderSchema = z.enum(["host", "cloudflare", "github", "gitlab", "stripe"]);
|
|
685
|
+
export type InventoryProvider = z.infer<typeof InventoryProviderSchema>;
|
|
686
|
+
export const ServiceKindSchema = z.enum(["signoz", "outline", "paperless", "openproject", "invoiceninja", "infisical"]);
|
|
687
|
+
export type ServiceKind = z.infer<typeof ServiceKindSchema>;
|
|
688
|
+
// Non-secret option values the user provides; secret options (sshKey, apiToken, apiKey) are emitted as env()
|
|
689
|
+
// references and never travel over the wire.
|
|
690
|
+
export const InventoryValuesSchema = z.record(z.string(), z.union([z.string(), z.number()]));
|
|
691
|
+
// `const <name>` binding in deploy.config.ts, so it must be a valid identifier.
|
|
692
|
+
const inventoryName = z
|
|
693
|
+
.string()
|
|
694
|
+
.min(1)
|
|
695
|
+
.max(60)
|
|
696
|
+
.regex(/^[a-zA-Z_][a-zA-Z0-9_]*$/);
|
|
697
|
+
export const BackendEntrySchema = z.object({
|
|
698
|
+
kind: z.literal("backend"),
|
|
699
|
+
provider: InventoryProviderSchema,
|
|
700
|
+
name: z.string(),
|
|
701
|
+
values: InventoryValuesSchema,
|
|
702
|
+
});
|
|
703
|
+
export const ServiceEntrySchema = z.object({
|
|
704
|
+
kind: z.literal("service"),
|
|
705
|
+
service: ServiceKindSchema,
|
|
706
|
+
name: z.string(),
|
|
707
|
+
values: InventoryValuesSchema,
|
|
708
|
+
on: z.string(),
|
|
709
|
+
expose: z.string(),
|
|
710
|
+
});
|
|
711
|
+
// i.want.app — a deployable app built from source. Single production environment on `main`; `values.domain` is
|
|
712
|
+
// where it's exposed. Multi-env/teams/use wiring is hand-authored outside the managed region.
|
|
713
|
+
export const AppEntrySchema = z.object({
|
|
714
|
+
kind: z.literal("app"),
|
|
715
|
+
name: z.string(),
|
|
716
|
+
values: InventoryValuesSchema,
|
|
717
|
+
on: z.string(),
|
|
718
|
+
expose: z.string(),
|
|
719
|
+
});
|
|
720
|
+
export const InventoryEntrySchema = z.discriminatedUnion("kind", [BackendEntrySchema, ServiceEntrySchema, AppEntrySchema]);
|
|
721
|
+
export type InventoryEntry = z.infer<typeof InventoryEntrySchema>;
|
|
722
|
+
export const AddInventoryInputSchema = z.discriminatedUnion("kind", [
|
|
723
|
+
BackendEntrySchema.extend({ name: inventoryName }),
|
|
724
|
+
ServiceEntrySchema.extend({ name: inventoryName }),
|
|
725
|
+
AppEntrySchema.extend({ name: inventoryName }),
|
|
726
|
+
]);
|
|
727
|
+
export type AddInventoryInput = z.infer<typeof AddInventoryInputSchema>;
|
|
728
|
+
export const InventoryNameParamSchema = z.object({ name: z.string() });
|
|
729
|
+
export const InventoryListSchema = z.object({ entries: z.array(InventoryEntrySchema) });
|
|
730
|
+
|
|
731
|
+
// A deploy-target host self-registering via the connect-host script's POST /enroll (connect-token auth). The SSH
|
|
732
|
+
// key (+ optional Cloudflare token) is written to desired-state/.env; the host (+ cf) is upserted into inventory.
|
|
733
|
+
export const EnrollHostInputSchema = z.object({
|
|
734
|
+
name: inventoryName,
|
|
735
|
+
user: z.string().min(1),
|
|
736
|
+
address: z.string().min(1),
|
|
737
|
+
port: z.coerce.number().default(22),
|
|
738
|
+
via: z.enum(["direct", "cloudflared"]).default("cloudflared"),
|
|
739
|
+
sshKey: z.string().min(1),
|
|
740
|
+
cfToken: z.string().optional(),
|
|
741
|
+
// The zone the connect script resolved alongside cfToken — recorded on the i.have.cloudflare entry so
|
|
742
|
+
// resolve validates against it (no re-discovery) and the Add-service dialog offers `<subdomain>.<zone>`.
|
|
743
|
+
cfZone: z.string().optional(),
|
|
744
|
+
});
|
|
745
|
+
export type EnrollHostInput = z.infer<typeof EnrollHostInputSchema>;
|
|
746
|
+
|
|
747
|
+
// ---- capabilities: the sandbox's unified capability manifest (.intentic/capabilities.json) ----
|
|
748
|
+
// Everything a user adds to a sandbox is a capability with an idempotent apply + a status check. The manifest is
|
|
749
|
+
// the source of truth for what's active; `mcp`-kind entries also feed the agent's MCP servers each turn. DevOps
|
|
750
|
+
// is the capability that scaffolds the intent/desired-state repos — until it's active the sandbox is empty.
|
|
751
|
+
|
|
752
|
+
export const CapabilityKindSchema = z.enum([
|
|
753
|
+
"devops",
|
|
754
|
+
"monorepo",
|
|
755
|
+
"mcp",
|
|
756
|
+
"service",
|
|
757
|
+
"integration",
|
|
758
|
+
"cli",
|
|
759
|
+
"plugin",
|
|
760
|
+
"extension",
|
|
761
|
+
"ssh",
|
|
762
|
+
"vpn",
|
|
763
|
+
"docker",
|
|
764
|
+
"browser",
|
|
765
|
+
"agent",
|
|
766
|
+
]);
|
|
767
|
+
export type CapabilityKind = z.infer<typeof CapabilityKindSchema>;
|
|
768
|
+
export const CapabilityStateSchema = z.enum(["active", "pending", "error", "inactive"]);
|
|
769
|
+
export type CapabilityState = z.infer<typeof CapabilityStateSchema>;
|
|
770
|
+
|
|
771
|
+
// A manifest entry id (capabilities + automations) — also the `mcp__<id>__…` server name for mcp capabilities,
|
|
772
|
+
// so it's a safe identifier.
|
|
773
|
+
const entryId = z
|
|
774
|
+
.string()
|
|
775
|
+
.min(1)
|
|
776
|
+
.max(60)
|
|
777
|
+
.regex(/^[a-zA-Z0-9][a-zA-Z0-9_-]*$/);
|
|
778
|
+
|
|
779
|
+
// Per-kind config. Secrets (an mcp token) live here and are denylisted like tools.json.
|
|
780
|
+
export const McpConfigSchema = z.object({ url: z.string().url(), token: z.string().optional() });
|
|
781
|
+
export const ServiceConfigSchema = z.object({
|
|
782
|
+
service: ServiceKindSchema,
|
|
783
|
+
domain: z.string().min(1),
|
|
784
|
+
on: z.string().min(1),
|
|
785
|
+
expose: z.string().min(1),
|
|
786
|
+
});
|
|
787
|
+
// External-app credential injected into DEPLOYED apps (i.have.stripe → STRIPE_API_KEY from env). Agent-facing
|
|
788
|
+
// connectors are `cli` capabilities instead (see below), not integrations.
|
|
789
|
+
export const IntegrationConfigSchema = z.object({ provider: z.literal("stripe") });
|
|
790
|
+
// A `cli` capability gives the AGENT an authenticated command-line tool (not a deployed-app credential like
|
|
791
|
+
// `integration`): the credential + any non-secret URL are stored here and injected into the agent's env each
|
|
792
|
+
// turn (see cliEnvOf), and a .claude/skills/<id> cheatsheet teaches the agent to use it via curl. The provider
|
|
793
|
+
// data (fields, env, skill, image fragment) is DATA in an installed extension's `contributes.connectors`, not a
|
|
794
|
+
// per-provider schema arm — so the config is `provider` + arbitrary string fields, validated against the
|
|
795
|
+
// connector's declared fields at add-time (see the sandbox's connector-registry) rather than by this schema.
|
|
796
|
+
export const CliConfigSchema = z.object({ provider: z.string().min(1) }).catchall(z.string());
|
|
797
|
+
// A Claude Code plugin from a git repo. The daemon only owns the checkout; the Agent SDK's plugin loader reads
|
|
798
|
+
// its internals (skills/agents/hooks/commands/.mcp.json). `path` = subdirectory for plugins that live inside a
|
|
799
|
+
// marketplace/monorepo checkout. `token` = https auth for private repos (never echoed; becomes hasToken).
|
|
800
|
+
export const PluginConfigSchema = z.object({
|
|
801
|
+
url: z.string().url(),
|
|
802
|
+
// Branch / tag / commit sha to pin; absent = the default branch's HEAD.
|
|
803
|
+
ref: z.string().min(1).optional(),
|
|
804
|
+
path: z
|
|
805
|
+
.string()
|
|
806
|
+
.min(1)
|
|
807
|
+
.refine((value) => !value.split("/").includes(".."), { message: "path must stay inside the checkout" })
|
|
808
|
+
.optional(),
|
|
809
|
+
token: z.string().min(1).optional(),
|
|
810
|
+
});
|
|
811
|
+
// An intentic extension from a git repo (an intentic-extension.json checkout — UI bundle + agent contributions
|
|
812
|
+
// + processes). Unlike `plugin`, `ref` is a REQUIRED full commit sha: extension code runs trusted in the
|
|
813
|
+
// owner's browser, so the owner approves exactly the code that runs — pin by construction, updates are explicit
|
|
814
|
+
// re-adds at a new sha. `path`/`token` as in PluginConfigSchema.
|
|
815
|
+
export const ExtensionConfigSchema = z.object({
|
|
816
|
+
url: z.string().url(),
|
|
817
|
+
ref: z.string().regex(/^[0-9a-f]{40}$/, "ref must be a full 40-character commit sha"),
|
|
818
|
+
path: z
|
|
819
|
+
.string()
|
|
820
|
+
.min(1)
|
|
821
|
+
.refine((value) => !value.split("/").includes(".."), { message: "path must stay inside the checkout" })
|
|
822
|
+
.optional(),
|
|
823
|
+
token: z.string().min(1).optional(),
|
|
824
|
+
});
|
|
825
|
+
// A remote machine the AGENT can reach over SSH. One capability = one machine; the id is its ssh-config Host
|
|
826
|
+
// alias, so the agent runs `ssh <id> "…"`. The handler writes a per-machine config block + a 0600 key/password
|
|
827
|
+
// file under ~/.ssh (see the ssh handler), so — unlike `cli` — nothing is injected into the agent's env, and
|
|
828
|
+
// several machines never collide. Discriminated by auth so exactly one credential shape is required.
|
|
829
|
+
export const SshConfigSchema = z.discriminatedUnion("auth", [
|
|
830
|
+
z.object({
|
|
831
|
+
auth: z.literal("key"),
|
|
832
|
+
host: z.string().min(1),
|
|
833
|
+
port: z.coerce.number().default(22),
|
|
834
|
+
user: z.string().min(1),
|
|
835
|
+
privateKey: z.string().min(1),
|
|
836
|
+
}),
|
|
837
|
+
z.object({
|
|
838
|
+
auth: z.literal("password"),
|
|
839
|
+
host: z.string().min(1),
|
|
840
|
+
port: z.coerce.number().default(22),
|
|
841
|
+
user: z.string().min(1),
|
|
842
|
+
password: z.string().min(1),
|
|
843
|
+
}),
|
|
844
|
+
]);
|
|
845
|
+
// A WireGuard tunnel the agent's traffic rides. `config` is the pasted .conf ([Interface] + [Peer]) — it holds
|
|
846
|
+
// the private key, so it's a secret like an mcp token. `enabled` is the on/off toggle (its default is set by
|
|
847
|
+
// the app repo's catalog entry): "on" ⇒ apply brings the tunnel up and the daemon restores it on boot; "off" ⇒
|
|
848
|
+
// the conf is stored but the tunnel stays down. The id doubles as the wg interface name, hence the union arm
|
|
849
|
+
// below caps it at Linux's 15-char IFNAMSIZ limit.
|
|
850
|
+
export const VpnConfigSchema = z.object({ config: z.string().min(1), enabled: z.enum(["on", "off"]).default("on") });
|
|
851
|
+
// A logged-in browser session the AGENT drives via Playwright MCP tools — for social platforms whose APIs can't
|
|
852
|
+
// cover "all the actions" (X reads are paywalled; X community-join and YouTube community-posts have no API). No
|
|
853
|
+
// secret in the manifest: the session lives in a persisted Chromium profile under .intentic/browser/<platform>,
|
|
854
|
+
// established once through the guided-login WebSocket (/system/browser-login). Chromium itself rides this kind's
|
|
855
|
+
// Dockerfile fragment, applied on an owner rebuild. One capability = one platform (the id doubles as the profile).
|
|
856
|
+
export const BrowserPlatformSchema = z.enum(["reddit", "x", "youtube"]);
|
|
857
|
+
export const BrowserConfigSchema = z.object({ platform: BrowserPlatformSchema });
|
|
858
|
+
// An ACP (Agent Client Protocol) agent served as a chat provider: the daemon spawns `command` as a long-lived
|
|
859
|
+
// subprocess speaking JSON-RPC over stdio, and the capability id becomes the provider id in the chat picker
|
|
860
|
+
// (see AgentProviderSchema). `command` is split on whitespace — no shell quoting. `env` is a pasted KEY=VALUE
|
|
861
|
+
// block (one per line); credentials ride here, so the whole block is the secret field (echoed as hasSecret) —
|
|
862
|
+
// the vpn-conf precedent. `loginCommand` is an interactive login the user completes in a visible terminal
|
|
863
|
+
// (device-code flows); the agent persists credentials in its own store inside the container. `name` is the
|
|
864
|
+
// picker's display label; absent = the id.
|
|
865
|
+
export const AcpAgentConfigSchema = z.object({
|
|
866
|
+
command: z.string().min(1),
|
|
867
|
+
name: z.string().min(1).optional(),
|
|
868
|
+
env: z.string().optional(),
|
|
869
|
+
loginCommand: z.string().min(1).optional(),
|
|
870
|
+
});
|
|
871
|
+
export type McpConfig = z.infer<typeof McpConfigSchema>;
|
|
872
|
+
export type ServiceConfig = z.infer<typeof ServiceConfigSchema>;
|
|
873
|
+
export type IntegrationConfig = z.infer<typeof IntegrationConfigSchema>;
|
|
874
|
+
export type CliConfig = z.infer<typeof CliConfigSchema>;
|
|
875
|
+
export type PluginConfig = z.infer<typeof PluginConfigSchema>;
|
|
876
|
+
export type ExtensionConfig = z.infer<typeof ExtensionConfigSchema>;
|
|
877
|
+
export type SshConfig = z.infer<typeof SshConfigSchema>;
|
|
878
|
+
export type VpnConfig = z.infer<typeof VpnConfigSchema>;
|
|
879
|
+
export type BrowserPlatform = z.infer<typeof BrowserPlatformSchema>;
|
|
880
|
+
export type BrowserConfig = z.infer<typeof BrowserConfigSchema>;
|
|
881
|
+
export type AcpAgentConfig = z.infer<typeof AcpAgentConfigSchema>;
|
|
882
|
+
|
|
883
|
+
export const CapabilitySchema = z.discriminatedUnion("kind", [
|
|
884
|
+
z.object({ id: entryId, kind: z.literal("devops"), config: z.object({}) }),
|
|
885
|
+
// A pnpm+turbo monorepo the user scaffolds as its own repo; the `id` is the repo name. No config — apps are
|
|
886
|
+
// added into it afterwards from its operator panel.
|
|
887
|
+
z.object({ id: entryId, kind: z.literal("monorepo"), config: z.object({}) }),
|
|
888
|
+
z.object({ id: entryId, kind: z.literal("mcp"), config: McpConfigSchema }),
|
|
889
|
+
z.object({ id: entryId, kind: z.literal("service"), config: ServiceConfigSchema }),
|
|
890
|
+
z.object({ id: entryId, kind: z.literal("integration"), config: IntegrationConfigSchema }),
|
|
891
|
+
z.object({ id: entryId, kind: z.literal("cli"), config: CliConfigSchema }),
|
|
892
|
+
z.object({ id: entryId, kind: z.literal("plugin"), config: PluginConfigSchema }),
|
|
893
|
+
z.object({ id: entryId, kind: z.literal("extension"), config: ExtensionConfigSchema }),
|
|
894
|
+
z.object({ id: entryId, kind: z.literal("ssh"), config: SshConfigSchema }),
|
|
895
|
+
z.object({ id: entryId.max(15), kind: z.literal("vpn"), config: VpnConfigSchema }),
|
|
896
|
+
// The in-sandbox Docker Engine (baked into the base image, dormant by default). No config: the capability's
|
|
897
|
+
// whole effect is its fragment's `--privileged` runtime directive + running dockerd. No remove — the engine's
|
|
898
|
+
// state (/var/lib/docker) and whatever runs on it make a silent de-privilege more destructive than useful.
|
|
899
|
+
z.object({ id: entryId, kind: z.literal("docker"), config: z.object({}) }),
|
|
900
|
+
z.object({ id: entryId, kind: z.literal("browser"), config: BrowserConfigSchema }),
|
|
901
|
+
z.object({ id: entryId, kind: z.literal("agent"), config: AcpAgentConfigSchema }),
|
|
902
|
+
]);
|
|
903
|
+
export type Capability = z.infer<typeof CapabilitySchema>;
|
|
904
|
+
|
|
905
|
+
export const CapabilityStatusSchema = z.object({ state: CapabilityStateSchema, detail: z.string().optional() });
|
|
906
|
+
export type CapabilityStatus = z.infer<typeof CapabilityStatusSchema>;
|
|
907
|
+
// The list row: manifest entry + live status. Secrets are never returned (an mcp token becomes hasToken).
|
|
908
|
+
export const CapabilitySummarySchema = z.object({
|
|
909
|
+
id: z.string(),
|
|
910
|
+
kind: CapabilityKindSchema,
|
|
911
|
+
status: CapabilityStatusSchema,
|
|
912
|
+
config: z.record(z.string(), z.union([z.string(), z.number(), z.boolean()])),
|
|
913
|
+
});
|
|
914
|
+
export const CapabilitiesListSchema = z.object({ capabilities: z.array(CapabilitySummarySchema) });
|
|
915
|
+
export const CapabilityIdParamSchema = z.object({ id: z.string() });
|
|
916
|
+
// POST /capabilities/{id}/secret body: replace just the capability's secret field (its key is per-kind, see the
|
|
917
|
+
// sandbox's secretField) and re-run its idempotent apply — the /secrets page's edit path.
|
|
918
|
+
export const CapabilitySecretInputSchema = z.object({ id: z.string(), value: z.string().min(1) });
|
|
919
|
+
// POST /capabilities/{id}/login response: the interactive tmux session running the agent's loginCommand,
|
|
920
|
+
// which the web surfaces in the terminal panel for the user to complete the sign-in.
|
|
921
|
+
export const CapabilityLoginSchema = z.object({ session: z.string() });
|
|
922
|
+
|
|
923
|
+
// Browse a Claude Code plugin marketplace (a git repo with .claude-plugin/marketplace.json). POST so the
|
|
924
|
+
// optional token for a private marketplace never rides a URL or an access log.
|
|
925
|
+
export const MarketplaceRequestSchema = z.object({ url: z.string().url(), token: z.string().min(1).optional() });
|
|
926
|
+
// One marketplace entry; `install` is the entry's source resolved onto PluginConfig shape (url/ref/path), so
|
|
927
|
+
// picking an entry just pre-fills the plugin form. Absent = a source the daemon can't clone (e.g. npm).
|
|
928
|
+
export const MarketplacePluginSchema = z.object({
|
|
929
|
+
name: z.string(),
|
|
930
|
+
description: z.string().optional(),
|
|
931
|
+
version: z.string().optional(),
|
|
932
|
+
// "extension" marks an intentic-extension entry (installs as the `extension` capability, sha-pinned);
|
|
933
|
+
// absent/"plugin" = a Claude Code plugin. Claude Code ignores unknown marketplace fields, so one
|
|
934
|
+
// marketplace repo serves both consumers.
|
|
935
|
+
kind: z.enum(["plugin", "extension"]).optional(),
|
|
936
|
+
install: z.object({ url: z.string(), ref: z.string().optional(), path: z.string().optional() }).optional(),
|
|
937
|
+
});
|
|
938
|
+
export type MarketplacePlugin = z.infer<typeof MarketplacePluginSchema>;
|
|
939
|
+
export const MarketplaceSchema = z.object({ name: z.string(), plugins: z.array(MarketplacePluginSchema) });
|
|
940
|
+
export type Marketplace = z.infer<typeof MarketplaceSchema>;
|
|
941
|
+
|
|
942
|
+
// ---- extensions: installed extension-kind capabilities resolved to their manifests ----
|
|
943
|
+
// What the web extension host boots from: each row is an extension capability whose checkout still parses —
|
|
944
|
+
// the approved manifest (contribution declarations), and the checked-out commit (the code identity; the bundle
|
|
945
|
+
// route's ETag). A rotted checkout is skipped here; its capability row still shows status.
|
|
946
|
+
// The routing handle: a git-installed extension uses its capability entry id; an image-baked one has no
|
|
947
|
+
// capability entry and is addressed by the manifest-derived publisher.name — hence the dot in the pattern.
|
|
948
|
+
const extensionId = z
|
|
949
|
+
.string()
|
|
950
|
+
.min(1)
|
|
951
|
+
.max(121)
|
|
952
|
+
.regex(/^[a-zA-Z0-9][a-zA-Z0-9_.-]*$/);
|
|
953
|
+
export const ExtensionSummarySchema = z.object({
|
|
954
|
+
id: extensionId,
|
|
955
|
+
manifest: ExtensionManifestSchema,
|
|
956
|
+
commit: z.string(),
|
|
957
|
+
// Image-baked first-party extension (no git checkout, not removable) vs a git-installed capability — the
|
|
958
|
+
// web hides the uninstall affordance for baked ones.
|
|
959
|
+
builtin: z.boolean(),
|
|
960
|
+
});
|
|
961
|
+
export type ExtensionSummary = z.infer<typeof ExtensionSummarySchema>;
|
|
962
|
+
export const ExtensionsListSchema = z.object({ extensions: z.array(ExtensionSummarySchema) });
|
|
963
|
+
// The extension's contributes.settings values, persisted daemon-side (.intentic/extension-settings.json) keyed
|
|
964
|
+
// by the manifest-derived extension id — the checkout stays pristine, so a re-clone update never loses them.
|
|
965
|
+
// Secret-marked values are stripped from `settings`; `secretsSet` lists the secret keys that DO hold a value,
|
|
966
|
+
// so the UI renders "•••• (set)" without ever receiving the secret back.
|
|
967
|
+
export const ExtensionSettingsSchema = z.object({
|
|
968
|
+
settings: z.record(z.string(), z.union([z.string(), z.number(), z.boolean()])),
|
|
969
|
+
secretsSet: z.array(z.string()),
|
|
970
|
+
});
|
|
971
|
+
export type ExtensionSettings = z.infer<typeof ExtensionSettingsSchema>;
|
|
972
|
+
export const ExtensionSettingsInputSchema = z.object({
|
|
973
|
+
id: z.string(),
|
|
974
|
+
settings: z.record(z.string(), z.union([z.string(), z.number(), z.boolean()])),
|
|
975
|
+
});
|
|
976
|
+
// One declared background process (contributes.processes) — status/start/stop, addressed by the capability
|
|
977
|
+
// entry id + the manifest's process name. Undeclared names are NOT_FOUND, the manifest-honesty rule again.
|
|
978
|
+
export const ExtensionProcessParamSchema = z.object({ id: z.string(), name: z.string() });
|
|
979
|
+
export const ExtensionProcessStatusSchema = z.object({
|
|
980
|
+
name: z.string(),
|
|
981
|
+
running: z.boolean(),
|
|
982
|
+
port: z.number().optional(),
|
|
983
|
+
previewUrl: z.string().optional(),
|
|
984
|
+
});
|
|
985
|
+
export type ExtensionProcessStatus = z.infer<typeof ExtensionProcessStatusSchema>;
|
|
986
|
+
|
|
987
|
+
// ---- automations: scheduled agent wake-ups (.intentic/automations.json) ----
|
|
988
|
+
// An automation wakes the agent autonomously: the daemon's scheduler fires each enabled automation on its
|
|
989
|
+
// trigger, runs the optional guard command (a shell command in the workspace; non-zero exit skips the wake),
|
|
990
|
+
// then runs one agent turn with the prompt. The manifest is user config; run history is daemon-recorded.
|
|
991
|
+
|
|
992
|
+
// `schedule` fires on its cron; `event` fires when an external system POSTs /automations/{id}/fire?token=…
|
|
993
|
+
// (a plain Hono route — webhook bodies are arbitrary). The token is the webhook's own auth (senders can't do
|
|
994
|
+
// Google ID tokens): optional on input — the daemon generates one on upsert — and always present in stored and
|
|
995
|
+
// listed automations, so the owner's UI can render the copyable URL.
|
|
996
|
+
// `listener` fires from a realtime source's connection to the provider (an extension's gateway process holds
|
|
997
|
+
// it, e.g. Discord) — no cron, no token, never reachable via /fire. channelId narrows to one channel; absent ⇒
|
|
998
|
+
// every channel the bot can read. eventType narrows to one kind of event (a Discord message, a live voice
|
|
999
|
+
// utterance batch, or a finished voice transcript); absent ⇒ all event kinds the source emits. mentioned
|
|
1000
|
+
// narrows message events to those that @mention one of the workspace's bots or reply to a bot's message;
|
|
1001
|
+
// absent ⇒ all messages. `provider` and `eventType` are open strings — a realtime source is now extension-
|
|
1002
|
+
// declared (contributes.listener), so the daemon validates a listener trigger at upsert against `webchat` ∪ the
|
|
1003
|
+
// installed extensions' declared providers/eventTypes rather than a hardcoded enum here.
|
|
1004
|
+
// `webchat` is the exception: it has no gateway. An embeddable widget POSTs a visitor's message to
|
|
1005
|
+
// /webchat/<id>/message and the agent's reply streams back over SSE. Its address is the public automation id, so
|
|
1006
|
+
// allowedOrigins (the widget's embed sites) + a per-conversation rate limit are its abuse boundary — no secret
|
|
1007
|
+
// token can live in a browser.
|
|
1008
|
+
export const TriggerSchema = z.discriminatedUnion("kind", [
|
|
1009
|
+
z.object({ kind: z.literal("schedule"), cron: z.string().min(1) }),
|
|
1010
|
+
z.object({ kind: z.literal("event"), token: z.string().min(1).optional() }),
|
|
1011
|
+
z.object({
|
|
1012
|
+
kind: z.literal("listener"),
|
|
1013
|
+
provider: z.string().min(1),
|
|
1014
|
+
channelId: z.string().min(1).optional(),
|
|
1015
|
+
eventType: z.string().min(1).optional(),
|
|
1016
|
+
mentioned: z.boolean().optional(),
|
|
1017
|
+
// webchat only: the website origins allowed to POST to the widget endpoint. Absent/empty ⇒ none admitted.
|
|
1018
|
+
allowedOrigins: z.array(z.string()).optional(),
|
|
1019
|
+
}),
|
|
1020
|
+
]);
|
|
1021
|
+
export type Trigger = z.infer<typeof TriggerSchema>;
|
|
1022
|
+
|
|
1023
|
+
export const AutomationSchema = z.object({
|
|
1024
|
+
id: entryId,
|
|
1025
|
+
trigger: TriggerSchema,
|
|
1026
|
+
// Shell command run in the workspace root before waking; exit 0 ⇒ wake, non-zero ⇒ the run is "skipped".
|
|
1027
|
+
guard: z.string().min(1).optional(),
|
|
1028
|
+
prompt: z.string().min(1),
|
|
1029
|
+
// Which provider adapter serves the wake; absent ⇒ claude. Same dispatch as a chat turn (AgentTurnSchema.agent).
|
|
1030
|
+
agent: AgentProviderSchema.optional(),
|
|
1031
|
+
// Which harness (agentic loop) runs the wake; absent ⇒ native. Same semantics as AgentTurnSchema.harness.
|
|
1032
|
+
harness: AgentHarnessSchema.optional(),
|
|
1033
|
+
// Which model the wake runs on (see agent-catalog.ts modelsFor); absent ⇒ the provider's default.
|
|
1034
|
+
model: z.string().optional(),
|
|
1035
|
+
// When true, a fire doesn't wake the agent — it's held in the approvals queue until the owner approves.
|
|
1036
|
+
requireApproval: z.boolean().optional(),
|
|
1037
|
+
enabled: z.boolean(),
|
|
1038
|
+
});
|
|
1039
|
+
export type Automation = z.infer<typeof AutomationSchema>;
|
|
1040
|
+
|
|
1041
|
+
// A wake held for owner approval (.intentic/approvals/<id>.json, one file per held wake). It snapshots the
|
|
1042
|
+
// trigger payload so an approved run replays exactly what fired, even across a daemon restart. The id is minted
|
|
1043
|
+
// by the daemon (an entryId-safe filename).
|
|
1044
|
+
export const AutomationApprovalSchema = z.object({
|
|
1045
|
+
id: entryId,
|
|
1046
|
+
automationId: z.string(),
|
|
1047
|
+
// The event/listener payload the wake would have carried; absent for schedule triggers.
|
|
1048
|
+
payload: z.string().optional(),
|
|
1049
|
+
createdAt: z.number(),
|
|
1050
|
+
});
|
|
1051
|
+
export type AutomationApproval = z.infer<typeof AutomationApprovalSchema>;
|
|
1052
|
+
export const AutomationApprovalsListSchema = z.object({ approvals: z.array(AutomationApprovalSchema) });
|
|
1053
|
+
export const AutomationApprovalIdParamSchema = z.object({ id: z.string() });
|
|
1054
|
+
|
|
1055
|
+
export const AutomationRunSchema = z.object({
|
|
1056
|
+
at: z.number(),
|
|
1057
|
+
// skipped = the guard said no; error = the guard passed but the agent turn surfaced an error.
|
|
1058
|
+
outcome: z.enum(["completed", "skipped", "error"]),
|
|
1059
|
+
detail: z.string().optional(),
|
|
1060
|
+
});
|
|
1061
|
+
export type AutomationRun = z.infer<typeof AutomationRunSchema>;
|
|
1062
|
+
|
|
1063
|
+
// The list row: the stored automation + its recent runs + the next scheduled fire (absent when disabled).
|
|
1064
|
+
export const AutomationSummarySchema = AutomationSchema.extend({
|
|
1065
|
+
runs: z.array(AutomationRunSchema),
|
|
1066
|
+
nextRun: z.number().optional(),
|
|
1067
|
+
});
|
|
1068
|
+
export type AutomationSummary = z.infer<typeof AutomationSummarySchema>;
|
|
1069
|
+
export const AutomationsListSchema = z.object({ automations: z.array(AutomationSummarySchema) });
|
|
1070
|
+
export const AutomationIdParamSchema = z.object({ id: z.string() });
|
|
1071
|
+
|
|
1072
|
+
// ---- drafts: agent-proposed posts awaiting owner approval (.intentic/drafts/<id>.json) ----
|
|
1073
|
+
// One JSON file per draft. The AGENT creates drafts with its normal file tools — it can't call daemon routes,
|
|
1074
|
+
// the same split as the environment proposal — while the daemon edits/deletes them on the owner's behalf, so
|
|
1075
|
+
// the two writers never share a file. The id IS the filename (entryId charset ⇒ path-safe); the body never
|
|
1076
|
+
// carries it. Posting is the agent's job too (there is no typed publish path): a "publish approved drafts"
|
|
1077
|
+
// automation wakes the agent for due drafts, which posts via the platform skills and flips the status.
|
|
1078
|
+
|
|
1079
|
+
export const DraftStatusSchema = z.enum(["proposed", "approved", "posting", "posted", "failed"]);
|
|
1080
|
+
export type DraftStatus = z.infer<typeof DraftStatusSchema>;
|
|
1081
|
+
|
|
1082
|
+
// The on-disk file body. proposed (agent) → approved (owner) → posting (publisher, set BEFORE acting so a dead
|
|
1083
|
+
// turn can't double-post) → posted | failed. Reject = delete the file; retry = re-approve a failed draft.
|
|
1084
|
+
export const DraftSchema = z.object({
|
|
1085
|
+
// Which skill posts it: "x" | "reddit" | "youtube" | "discord" | … — a bare string so new platforms need
|
|
1086
|
+
// no contract change; an unknown platform simply fails at posting time.
|
|
1087
|
+
platform: z.string().min(1),
|
|
1088
|
+
content: z.string().min(1),
|
|
1089
|
+
// Reddit posts / YouTube uploads need one.
|
|
1090
|
+
title: z.string().optional(),
|
|
1091
|
+
// Where on the platform: subreddit / Discord channel id / community.
|
|
1092
|
+
target: z.string().optional(),
|
|
1093
|
+
// Workspace-relative attachment paths, e.g. ".intentic/drafts/media/chart.png".
|
|
1094
|
+
media: z.array(z.string()).optional(),
|
|
1095
|
+
// Suggested post time (epoch ms, the at/nextRun convention). Optional — the agent may propose without a
|
|
1096
|
+
// date and the owner sets one at approval; an approved draft with no date posts as soon as it's picked up.
|
|
1097
|
+
scheduledAt: z.number().optional(),
|
|
1098
|
+
// Agent-written files only need platform + content; status defaults, the rest are optional, so a
|
|
1099
|
+
// well-formed proposal never lands in `invalid` just for omitting bookkeeping fields.
|
|
1100
|
+
status: DraftStatusSchema.default("proposed"),
|
|
1101
|
+
createdAt: z.number().optional(),
|
|
1102
|
+
postedAt: z.number().optional(),
|
|
1103
|
+
// Why posting failed; set with status "failed".
|
|
1104
|
+
error: z.string().optional(),
|
|
1105
|
+
});
|
|
1106
|
+
export type Draft = z.infer<typeof DraftSchema>;
|
|
1107
|
+
|
|
1108
|
+
// The list row / upsert input: the file body plus its filename id.
|
|
1109
|
+
export const DraftSummarySchema = DraftSchema.extend({ id: entryId });
|
|
1110
|
+
export type DraftSummary = z.infer<typeof DraftSummarySchema>;
|
|
1111
|
+
// `invalid` = filenames that failed to parse. Agent-written files are a trust boundary — without this a typo'd
|
|
1112
|
+
// draft would silently never post.
|
|
1113
|
+
export const DraftsListSchema = z.object({ drafts: z.array(DraftSummarySchema), invalid: z.array(z.string()) });
|
|
1114
|
+
// entryId, not a bare string: the id becomes a filename under .intentic/drafts/.
|
|
1115
|
+
export const DraftIdParamSchema = z.object({ id: entryId });
|
|
1116
|
+
|
|
1117
|
+
// ---- panels: per-repository dev servers + the content facts extensions detect on ----
|
|
1118
|
+
// Every discovered git repo under /work is one list row: its runnable-panel runtime status (a `dev` script at
|
|
1119
|
+
// operator/ or the repo root; the daemon runs it, auto-assigns a free port, and the preview proxy routes
|
|
1120
|
+
// preview-<panelKey>-<sandboxId>.<zone> to it) PLUS content facts — evidence the web app's extensions run their
|
|
1121
|
+
// detect() over, computed daemon-side in one pass so the browser never scans /work file-by-file.
|
|
1122
|
+
|
|
1123
|
+
export const PanelSummarySchema = z.object({
|
|
1124
|
+
// The repo id: its root-relative dir under /work (slashes become `--` in the preview subdomain label).
|
|
1125
|
+
repo: z.string(),
|
|
1126
|
+
// Whether the repo ships a runnable dev server (a package.json `dev` script at operator/ or the root).
|
|
1127
|
+
hasPanel: z.boolean(),
|
|
1128
|
+
running: z.boolean(),
|
|
1129
|
+
// A plain probe of the running panel's port; false when not running.
|
|
1130
|
+
healthy: z.boolean(),
|
|
1131
|
+
// The dev server's OS-assigned port; absent when not running.
|
|
1132
|
+
port: z.number().optional(),
|
|
1133
|
+
// https://preview-<repo>-<sandboxId>.<zone>; absent when the sandbox has no zone or connect token (loopback/tests).
|
|
1134
|
+
previewUrl: z.string().optional(),
|
|
1135
|
+
// The workspace role this repo dir occupies (the three fixed dirs); absent for extra clones.
|
|
1136
|
+
role: z.enum(["intent", "desired-state", "app"]).optional(),
|
|
1137
|
+
// Content facts: deploy.config.ts (the intent ledger's day-one marker), desired-state.json (present after
|
|
1138
|
+
// the first resolve), .intentic/ui/index.html (a sandboxed directory UI), pnpm-workspace.yaml +
|
|
1139
|
+
// turbo.json (a pnpm+turbo monorepo), and vitest evidence (a root vitest.config.ts, or "vitest" in the
|
|
1140
|
+
// root manifest / workspace catalog).
|
|
1141
|
+
deployConfig: z.boolean(),
|
|
1142
|
+
desiredState: z.boolean(),
|
|
1143
|
+
directoryUi: z.boolean(),
|
|
1144
|
+
monorepo: z.boolean(),
|
|
1145
|
+
vitest: z.boolean(),
|
|
1146
|
+
});
|
|
1147
|
+
export type PanelSummary = z.infer<typeof PanelSummarySchema>;
|
|
1148
|
+
export const PanelsListSchema = z.object({ panels: z.array(PanelSummarySchema) });
|
|
1149
|
+
export type PanelsList = z.infer<typeof PanelsListSchema>;
|
|
1150
|
+
// The {repo} path param on the start/stop/terminals routes (a bare string: unknown repo is a handler NOT_FOUND).
|
|
1151
|
+
export const PanelRepoParamSchema = z.object({ repo: z.string() });
|
|
1152
|
+
|
|
1153
|
+
// ---- ports: every listening TCP socket in the sandbox + explicit port forwarding ----
|
|
1154
|
+
// Anything run in a terminal (a turbo TUI fanning out dev servers, `python -m http.server`, an agent's ad-hoc
|
|
1155
|
+
// process) binds ports the daemon never assigned — the panel machinery can't see them. The /ports routes are
|
|
1156
|
+
// the generic complement: `list` reports the live listeners (procfs scan, on demand), `forward` makes one
|
|
1157
|
+
// reachable at port-<slot>-<sandboxId>.<zone> through the preview proxy. Forwarding is an explicit gesture —
|
|
1158
|
+
// previews are public, so nothing is exposed until the owner (or an agent acting for them) asks.
|
|
1159
|
+
|
|
1160
|
+
export const PortSummarySchema = z.object({
|
|
1161
|
+
port: z.number(),
|
|
1162
|
+
// The loopback address the listener actually answers at inside the sandbox — a `localhost` bind can land
|
|
1163
|
+
// on ::1 only (Vite). The preview proxy and the desktop mirror (Mutagen forward) both dial this.
|
|
1164
|
+
host: z.enum(["127.0.0.1", "::1"]),
|
|
1165
|
+
// Which bucket the Ports view files it under: `workspace` = user-run (dev servers in repos, terminal
|
|
1166
|
+
// processes, published container ports) — the previewable set; `system` = the sandbox's own machinery
|
|
1167
|
+
// (agent runtimes, translator, dockerd, sshd), listed for transparency but nobody previews it.
|
|
1168
|
+
kind: z.enum(["workspace", "system"]),
|
|
1169
|
+
// The owning process, resolved from procfs; absent when no /proc/*/fd entry matched the socket's inode.
|
|
1170
|
+
pid: z.number().optional(),
|
|
1171
|
+
// The process argv joined with spaces ("node /work/app/node_modules/.bin/vite").
|
|
1172
|
+
command: z.string().optional(),
|
|
1173
|
+
// The process working directory (how the UI attributes a port to a repo).
|
|
1174
|
+
cwd: z.string().optional(),
|
|
1175
|
+
forwarded: z.boolean(),
|
|
1176
|
+
// https://port-<slot>-<sandboxId>.<zone>; present only while forwarded AND the sandbox has a zone + id.
|
|
1177
|
+
previewUrl: z.string().optional(),
|
|
1178
|
+
});
|
|
1179
|
+
export type PortSummary = z.infer<typeof PortSummarySchema>;
|
|
1180
|
+
export const PortsListSchema = z.object({ ports: z.array(PortSummarySchema) });
|
|
1181
|
+
export type PortsList = z.infer<typeof PortsListSchema>;
|
|
1182
|
+
|
|
1183
|
+
export const PortParamSchema = z.object({ port: z.number().int().min(1).max(65535) });
|
|
1184
|
+
// `previewUrl` is absent on a loopback/no-tunnel sandbox — the slot is mapped, but no public hostname exists.
|
|
1185
|
+
export const PortForwardResultSchema = z.object({ previewUrl: z.string().optional() });
|
|
1186
|
+
export type PortForwardResult = z.infer<typeof PortForwardResultSchema>;
|
|
1187
|
+
|
|
1188
|
+
// ---- terminal ----
|
|
1189
|
+
// EVERY attachable tmux session in the sandbox — the web app's ONE global terminal panel (the interactive I/O
|
|
1190
|
+
// is the /system/terminal WebSocket, not oRPC): `shell` = a web-* session the user opened (numbered pill),
|
|
1191
|
+
// `panel` = a panel-* dev-server session (labeled by its panel key, started via Start; running:false =
|
|
1192
|
+
// untracked, e.g. a finished one-shot job's lingering shell), `agent` = an agent-* session the Claude agent's
|
|
1193
|
+
// Bash commands run in (live-watchable, AI-marked in the UI), `job` = a job-* session the daemon's terminal
|
|
1194
|
+
// runner executes user-triggered flows in (capability adds, infra check), `process` = a managed background
|
|
1195
|
+
// process riding a panel session (an extension's declared processes, dockerd) — surfaced in the panel's
|
|
1196
|
+
// background-processes popover with read-only log views, never as a killable tab; running is the actual
|
|
1197
|
+
// process (a lingering shell after a crash reads false). A process row that maps to an installed extension's
|
|
1198
|
+
// declared process carries extensionId+processName, the address for its /extensions start/stop routes. The
|
|
1199
|
+
// `{name}` kill-route param is a bare string validated in the handler (a bad name is a BAD_REQUEST) since the
|
|
1200
|
+
// same charset gates a `tmux kill-session -t` shell-out.
|
|
1201
|
+
export const TerminalSessionSchema = z.object({
|
|
1202
|
+
name: z.string(),
|
|
1203
|
+
label: z.string().optional(),
|
|
1204
|
+
kind: z.enum(["shell", "panel", "agent", "job", "process"]),
|
|
1205
|
+
running: z.boolean(),
|
|
1206
|
+
extensionId: z.string().optional(),
|
|
1207
|
+
processName: z.string().optional(),
|
|
1208
|
+
});
|
|
1209
|
+
export const TerminalsListSchema = z.object({ sessions: z.array(TerminalSessionSchema) });
|
|
1210
|
+
export type TerminalsList = z.infer<typeof TerminalsListSchema>;
|
|
1211
|
+
export const TerminalNameParamSchema = z.object({ name: z.string() });
|
|
1212
|
+
|
|
1213
|
+
// ---- environment: the overlay Dockerfile extending the sandbox image ----
|
|
1214
|
+
// The approved file is DAEMON-COMPOSED: pinned FROM + capability fragments + the owner-approved custom section.
|
|
1215
|
+
// The agent writes the proposal file (.intentic/environment.Dockerfile — custom-section content only, no FROM)
|
|
1216
|
+
// with its normal file tools; the owner approves it in the browser, which stores it as the custom file and
|
|
1217
|
+
// recomposes the approved artifact whose sha256 the rebuild executor pins. Status is derived, never stored:
|
|
1218
|
+
// applied = sha256(approved) === appliedHash; pending rebuild = approved present but hashes differ; proposed =
|
|
1219
|
+
// proposal present with a hash different from custom's.
|
|
1220
|
+
|
|
1221
|
+
const environmentFileSchema = z.object({ content: z.string(), hash: z.string() });
|
|
1222
|
+
export const EnvironmentSchema = z.object({
|
|
1223
|
+
proposal: environmentFileSchema.optional(),
|
|
1224
|
+
// The owner-approved agent-written custom section (.intentic/environment.custom.Dockerfile).
|
|
1225
|
+
custom: environmentFileSchema.optional(),
|
|
1226
|
+
approved: environmentFileSchema.optional(),
|
|
1227
|
+
// sha256 of the overlay the running container was built from (SANDBOX_ENVIRONMENT_HASH); absent = stock image.
|
|
1228
|
+
appliedHash: z.string().optional(),
|
|
1229
|
+
// config.sandbox.name — the UI derives the rebuild one-liner's slug from it.
|
|
1230
|
+
container: z.string().optional(),
|
|
1231
|
+
});
|
|
1232
|
+
export type Environment = z.infer<typeof EnvironmentSchema>;
|
|
1233
|
+
export const EnvironmentApproveSchema = z.object({ hash: z.string().min(1) });
|
|
1234
|
+
|
|
1235
|
+
// ---- secrets: user-supplied env-var secrets the daemon writes to desired-state/.env ----
|
|
1236
|
+
// The web posts a Cloudflare token / GitHub PAT / another-host SSH key straight to the sandbox daemon (never
|
|
1237
|
+
// through the platform); `apply` reloads .env each run so a new secret is picked up with NO restart. `list`
|
|
1238
|
+
// returns KEYS ONLY — the values never leave the sandbox; `reveal` is the one deliberate, owner-only exception.
|
|
1239
|
+
export const SecretSetSchema = z.object({
|
|
1240
|
+
key: z
|
|
1241
|
+
.string()
|
|
1242
|
+
.regex(/^[A-Za-z_][A-Za-z0-9_]*$/)
|
|
1243
|
+
.max(128),
|
|
1244
|
+
value: z.string().min(1),
|
|
1245
|
+
});
|
|
1246
|
+
export const SecretKeysSchema = z.object({ keys: z.array(z.string()) });
|
|
1247
|
+
export const SecretKeyParamSchema = z.object({ key: z.string() });
|
|
1248
|
+
export const SecretRevealSchema = z.object({ value: z.string() });
|
|
1249
|
+
|
|
1250
|
+
// One entry per secret the sandbox knows about, across every store: intent env secrets and intentic-generated
|
|
1251
|
+
// passwords (from the desired-state repo), capability credentials, and AI-provider accounts. Values never ride
|
|
1252
|
+
// this shape — `revealable` says whether `reveal` can return one (everything but provider accounts).
|
|
1253
|
+
export const SecretInventoryEntrySchema = z.object({
|
|
1254
|
+
// Env-var key for env|generated; `<provider>:<accountId>` for provider entries; capability instance id
|
|
1255
|
+
// otherwise. Unique within the inventory — several accounts of one provider each get their own entry.
|
|
1256
|
+
key: z.string(),
|
|
1257
|
+
kind: z.enum(["env", "generated", "capability", "provider"]),
|
|
1258
|
+
// Display name for provider entries: "<ProviderName> · <accountLabel>". Absent on env/generated entries.
|
|
1259
|
+
label: z.string().optional(),
|
|
1260
|
+
status: z.enum(["missing", "set", "connected"]),
|
|
1261
|
+
// The artifact resources referencing this secret ({$secret} refs); [] for capability/provider entries.
|
|
1262
|
+
requiredBy: z.array(z.object({ resourceId: z.string(), type: z.string() })),
|
|
1263
|
+
// Human-readable provenance, e.g. "desired-state/.env" — the UI's "where does this live" line.
|
|
1264
|
+
storedAt: z.string(),
|
|
1265
|
+
revealable: z.boolean(),
|
|
1266
|
+
// Forgejo Actions replication state, present only after adopt on env|generated entries.
|
|
1267
|
+
ci: z.object({ synced: z.boolean(), pushedAt: z.string().optional() }).optional(),
|
|
1268
|
+
});
|
|
1269
|
+
export type SecretInventoryEntry = z.infer<typeof SecretInventoryEntrySchema>;
|
|
1270
|
+
export const SecretInventorySchema = z.object({ entries: z.array(SecretInventoryEntrySchema) });
|
|
1271
|
+
|
|
1272
|
+
// ---- system ----
|
|
1273
|
+
|
|
1274
|
+
// version: what this daemon runs (baked). latest/updateAvailable: the daemon compares its version to the
|
|
1275
|
+
// latest published `stable` release so the web can offer a non-blocking update (see system/version-check.ts).
|
|
1276
|
+
export const InfoSchema = z.object({
|
|
1277
|
+
name: z.string().optional(),
|
|
1278
|
+
image: z.string().optional(),
|
|
1279
|
+
version: z.string().optional(),
|
|
1280
|
+
latest: z.string().optional(),
|
|
1281
|
+
updateAvailable: z.boolean().optional(),
|
|
1282
|
+
});
|
|
1283
|
+
|
|
1284
|
+
// Intentic-provided host SSH tunnel: minting it needs intentic's PLATFORM Cloudflare account, so the daemon
|
|
1285
|
+
// can't do it directly — it relays to the platform authenticated by the connect token (the announce pattern).
|
|
1286
|
+
// The panel embeds the returned connector token + hostname in its connect-host one-liner.
|
|
1287
|
+
export const HostTunnelInputSchema = z.object({ hostName: z.string().min(1) });
|
|
1288
|
+
export const HostTunnelSchema = z.object({ hostname: z.string(), tunnelToken: z.string() });
|
|
1289
|
+
|
|
1290
|
+
// ---- activity: the activity audit log (historyRoot/activity.jsonl) ----
|
|
1291
|
+
// One provider-agnostic event per agent↔provider interaction, appended by the daemon only (never the agent —
|
|
1292
|
+
// the log lives under historyRoot, outside /work, so the agent can't read or rewrite its own trail). Discord
|
|
1293
|
+
// is the first source; other cli providers reuse the same shape.
|
|
1294
|
+
|
|
1295
|
+
export const ActivityEventSchema = z.object({
|
|
1296
|
+
id: z.string(),
|
|
1297
|
+
// Epoch ms; also the paging cursor.
|
|
1298
|
+
at: z.number(),
|
|
1299
|
+
// "discord", …; absent on provider-less system events (a cron automation.run).
|
|
1300
|
+
provider: z.string().optional(),
|
|
1301
|
+
// Which provider account handled the turn — the attribution key for per-account usage totals. Absent on
|
|
1302
|
+
// provider-less events and turns that ran on the provider's default account.
|
|
1303
|
+
account: z.string().optional(),
|
|
1304
|
+
direction: z.enum(["in", "out", "system"]),
|
|
1305
|
+
// in: message.received | voice_utterance.received | voice_transcript.received
|
|
1306
|
+
// out: message.send | reaction.add | messages.read | api.call (unclassified provider endpoint)
|
|
1307
|
+
// system: gateway.login_failed | dispatch.failed | voice.session_started | voice.session_ended | automation.run
|
|
1308
|
+
// | turn.started | turn.plan | turn.error | turn.completed (agent turn lifecycle; provider = claude/codex)
|
|
1309
|
+
type: z.string(),
|
|
1310
|
+
channelId: z.string().optional(),
|
|
1311
|
+
// Inbound author display name.
|
|
1312
|
+
author: z.string().optional(),
|
|
1313
|
+
// Full message text (inbound) or sent payload content (outbound).
|
|
1314
|
+
content: z.string().optional(),
|
|
1315
|
+
// Outbound HTTP method + endpoint path (tokens ride headers, never URLs).
|
|
1316
|
+
method: z.string().optional(),
|
|
1317
|
+
endpoint: z.string().optional(),
|
|
1318
|
+
// The agent turn that made/handled it — the join key between an inbound wake and its outbound calls.
|
|
1319
|
+
sessionId: z.string().optional(),
|
|
1320
|
+
automationIds: z.array(z.string()).optional(),
|
|
1321
|
+
outcome: z.enum(["ok", "error"]).optional(),
|
|
1322
|
+
error: z.string().optional(),
|
|
1323
|
+
// Source-specific detail: guildId, attachments, transcript path, participants…
|
|
1324
|
+
extra: z.record(z.string(), z.unknown()).optional(),
|
|
1325
|
+
});
|
|
1326
|
+
export type ActivityEvent = z.infer<typeof ActivityEventSchema>;
|
|
1327
|
+
|
|
1328
|
+
export const ActivityQuerySchema = z.object({
|
|
1329
|
+
provider: z.string().optional(),
|
|
1330
|
+
limit: z.coerce.number().min(1).max(500).default(100),
|
|
1331
|
+
// `at` cursor, exclusive — newest-first paging.
|
|
1332
|
+
before: z.coerce.number().optional(),
|
|
1333
|
+
});
|
|
1334
|
+
export type ActivityQuery = z.infer<typeof ActivityQuerySchema>;
|
|
1335
|
+
export const ActivityListSchema = z.object({ events: z.array(ActivityEventSchema) });
|
|
1336
|
+
|
|
1337
|
+
// Live connection health, probed per provider capability (not stored): gateway state from the client pool
|
|
1338
|
+
// (idle = the gateway is up but has no enabled listener automation to connect for — distinct from a
|
|
1339
|
+
// connection that should be up but isn't), lastError from the newest system-error event in the recent log.
|
|
1340
|
+
export const ActivityConnectionSchema = z.object({
|
|
1341
|
+
capabilityId: z.string(),
|
|
1342
|
+
provider: z.string(),
|
|
1343
|
+
gateway: z.enum(["ready", "connecting", "disconnected", "idle"]),
|
|
1344
|
+
lastError: z.string().optional(),
|
|
1345
|
+
});
|
|
1346
|
+
export const ActivityStatusSchema = z.object({
|
|
1347
|
+
connections: z.array(ActivityConnectionSchema),
|
|
1348
|
+
// The daemon's live voice session, when one is up.
|
|
1349
|
+
voice: z.object({ channelId: z.string(), channelName: z.string(), startedAt: z.number(), participants: z.array(z.string()) }).optional(),
|
|
1350
|
+
});
|
|
1351
|
+
export type ActivityStatus = z.infer<typeof ActivityStatusSchema>;
|
|
1352
|
+
|
|
1353
|
+
// ---- usage: per-account token/cost totals ----
|
|
1354
|
+
// Aggregated from the activity log's turn.completed events (their `usage` extra), grouped by provider+account.
|
|
1355
|
+
// Totals cover the retained log window (the log prunes to its most recent entries), not all-time.
|
|
1356
|
+
export const UsageAccountSchema = z.object({
|
|
1357
|
+
provider: z.string(),
|
|
1358
|
+
account: z.string(),
|
|
1359
|
+
turns: z.number(),
|
|
1360
|
+
inputTokens: z.number(),
|
|
1361
|
+
outputTokens: z.number(),
|
|
1362
|
+
cacheReadTokens: z.number(),
|
|
1363
|
+
cacheCreationTokens: z.number(),
|
|
1364
|
+
costUsd: z.number(),
|
|
1365
|
+
});
|
|
1366
|
+
export type UsageAccount = z.infer<typeof UsageAccountSchema>;
|
|
1367
|
+
export const UsageSummarySchema = z.object({ accounts: z.array(UsageAccountSchema) });
|
|
1368
|
+
|
|
1369
|
+
// ---- logs: daemon-owned debug logs (historyRoot/logs) ----
|
|
1370
|
+
// Terminal pipe-pane captures (terminals/), intentic CLI run logs (intentic-runs/), and the daemon's own pino
|
|
1371
|
+
// file (daemon.log) — written by the daemon/tmux only, under historyRoot so the agent can't rewrite them.
|
|
1372
|
+
|
|
1373
|
+
export const LogFileEntrySchema = z.object({
|
|
1374
|
+
// Path relative to the logs root, e.g. "terminals/web-1-%0.log" or "daemon.log".
|
|
1375
|
+
name: z.string(),
|
|
1376
|
+
sizeBytes: z.number(),
|
|
1377
|
+
// Epoch ms mtime.
|
|
1378
|
+
modifiedAt: z.number(),
|
|
1379
|
+
});
|
|
1380
|
+
export type LogFileEntry = z.infer<typeof LogFileEntrySchema>;
|
|
1381
|
+
export const LogsListSchema = z.object({ files: z.array(LogFileEntrySchema) });
|
|
1382
|
+
|
|
1383
|
+
// `name` rides the query (log names contain slashes, which don't fit a path segment); `bytes` is the tail
|
|
1384
|
+
// size — the newest bytes win when the file is larger.
|
|
1385
|
+
export const LogReadQuerySchema = z.object({
|
|
1386
|
+
name: z.string().min(1),
|
|
1387
|
+
bytes: z.coerce.number().min(1).max(1_048_576).default(65_536),
|
|
1388
|
+
});
|
|
1389
|
+
export const LogReadSchema = z.object({
|
|
1390
|
+
name: z.string(),
|
|
1391
|
+
sizeBytes: z.number(),
|
|
1392
|
+
// The tail text; truncated when the file holds more than the requested bytes.
|
|
1393
|
+
text: z.string(),
|
|
1394
|
+
truncated: z.boolean(),
|
|
1395
|
+
});
|
|
1396
|
+
export type LogRead = z.infer<typeof LogReadSchema>;
|
|
1397
|
+
|
|
1398
|
+
// A tab's self-report of what it is looking at, keyed by its /events connection's clientId. Full replace,
|
|
1399
|
+
// not a merge — an absent field means "cleared", so a tab leaving a file drops the path with the same report.
|
|
1400
|
+
export const PresenceReportSchema = z.object({
|
|
1401
|
+
clientId: z.string(),
|
|
1402
|
+
idle: z.boolean(),
|
|
1403
|
+
view: z.string().optional(),
|
|
1404
|
+
sessionId: z.string().optional(),
|
|
1405
|
+
path: z.string().optional(),
|
|
1406
|
+
});
|
|
1407
|
+
export type PresenceReport = z.infer<typeof PresenceReportSchema>;
|