@intentic/sandbox-contract 1.152.0 → 1.154.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/contracts/agent.contract.d.ts +9 -0
- package/dist/contracts/agent.contract.d.ts.map +1 -1
- package/dist/contracts/agent.contract.js +2 -1
- package/dist/contracts/agent.contract.js.map +1 -1
- package/dist/contracts/agents.contract.d.ts +2 -1
- package/dist/contracts/agents.contract.d.ts.map +1 -1
- package/dist/contracts/agents.contract.js +2 -2
- package/dist/contracts/agents.contract.js.map +1 -1
- package/dist/contracts/capabilities.contract.d.ts +13 -0
- package/dist/contracts/capabilities.contract.d.ts.map +1 -1
- package/dist/contracts/git.contract.d.ts +109 -4
- package/dist/contracts/git.contract.d.ts.map +1 -1
- package/dist/contracts/git.contract.js +10 -2
- package/dist/contracts/git.contract.js.map +1 -1
- package/dist/contracts/push.contract.d.ts +26 -0
- package/dist/contracts/push.contract.d.ts.map +1 -0
- package/dist/contracts/push.contract.js +9 -0
- package/dist/contracts/push.contract.js.map +1 -0
- package/dist/contracts/settings.contract.d.ts +18 -18
- package/dist/contracts/system.contract.d.ts +4 -4
- package/dist/contracts/usage.contract.d.ts +23 -0
- package/dist/contracts/usage.contract.d.ts.map +1 -0
- package/dist/contracts/usage.contract.js +6 -0
- package/dist/contracts/usage.contract.js.map +1 -0
- package/dist/contracts/vpn.contract.d.ts +2 -0
- package/dist/contracts/vpn.contract.d.ts.map +1 -1
- package/dist/events.d.ts +10 -0
- package/dist/events.d.ts.map +1 -1
- package/dist/events.js +3 -1
- package/dist/events.js.map +1 -1
- package/dist/index.d.ts +202 -23
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +6 -0
- package/dist/index.js.map +1 -1
- package/dist/schemas.d.ts +327 -13
- package/dist/schemas.d.ts.map +1 -1
- package/dist/schemas.js +117 -20
- package/dist/schemas.js.map +1 -1
- package/package.json +2 -2
- package/src/contracts/agent.contract.ts +4 -1
- package/src/contracts/agents.contract.ts +7 -6
- package/src/contracts/git.contract.ts +22 -1
- package/src/contracts/push.contract.ts +17 -0
- package/src/contracts/usage.contract.ts +10 -0
- package/src/events.ts +8 -3
- package/src/index.ts +6 -0
- package/src/schemas.test.ts +40 -0
- package/src/schemas.ts +287 -33
package/src/events.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
|
-
import { AgentSummarySchema, PermissionModeSchema, RateLimitInfoSchema } from "./schemas.js";
|
|
2
|
+
import { AgentProviderSchema, AgentSummarySchema, PermissionModeSchema, RateLimitInfoSchema } from "./schemas.js";
|
|
3
3
|
|
|
4
4
|
// The wire shapes streamed from the daemon's event-iterator procedures. This is their canonical home: the
|
|
5
5
|
// daemon yields them and the browser client consumes them from the same schema, so the two can't drift (they
|
|
@@ -41,8 +41,9 @@ export const PermissionAskSchema = z.object({
|
|
|
41
41
|
});
|
|
42
42
|
export type PermissionAsk = z.infer<typeof PermissionAskSchema>;
|
|
43
43
|
|
|
44
|
-
// One provider-advertised slash command
|
|
45
|
-
//
|
|
44
|
+
// One provider-advertised slash command — an ACP agent's available_commands entry, or a Claude Code session's
|
|
45
|
+
// supportedCommands() (its built-ins plus the workspace's own .claude/commands and any plugin/skill commands).
|
|
46
|
+
// `hint` is the argument placeholder the popover shows after the name.
|
|
46
47
|
export const AgentCommandSchema = z.object({
|
|
47
48
|
name: z.string(),
|
|
48
49
|
description: z.string(),
|
|
@@ -50,6 +51,10 @@ export const AgentCommandSchema = z.object({
|
|
|
50
51
|
});
|
|
51
52
|
export type AgentCommand = z.infer<typeof AgentCommandSchema>;
|
|
52
53
|
|
|
54
|
+
// GET /agent/commands — which provider's last-published list to read; absent = claude, matching AgentTurn.
|
|
55
|
+
export const AgentCommandsQuerySchema = z.object({ agent: AgentProviderSchema.optional() });
|
|
56
|
+
export const AgentCommandsSchema = z.object({ commands: z.array(AgentCommandSchema) });
|
|
57
|
+
|
|
53
58
|
// One TodoWrite/Task checklist item, surfaced live so the UI shows the agent's plan-of-work (Claude Code style).
|
|
54
59
|
export const TodoItemSchema = z.object({
|
|
55
60
|
content: z.string(),
|
package/src/index.ts
CHANGED
|
@@ -17,11 +17,13 @@ import { kimiContract } from "./contracts/kimi.contract.js";
|
|
|
17
17
|
import { logsContract } from "./contracts/logs.contract.js";
|
|
18
18
|
import { panelsContract } from "./contracts/panels.contract.js";
|
|
19
19
|
import { portsContract } from "./contracts/ports.contract.js";
|
|
20
|
+
import { pushContract } from "./contracts/push.contract.js";
|
|
20
21
|
import { secretsContract } from "./contracts/secrets.contract.js";
|
|
21
22
|
import { sessionsContract } from "./contracts/sessions.contract.js";
|
|
22
23
|
import { settingsContract } from "./contracts/settings.contract.js";
|
|
23
24
|
import { systemContract } from "./contracts/system.contract.js";
|
|
24
25
|
import { translatorContract } from "./contracts/translator.contract.js";
|
|
26
|
+
import { usageContract } from "./contracts/usage.contract.js";
|
|
25
27
|
import { vpnContract } from "./contracts/vpn.contract.js";
|
|
26
28
|
import { workspaceContract } from "./contracts/workspace.contract.js";
|
|
27
29
|
|
|
@@ -44,11 +46,13 @@ export { kimiContract } from "./contracts/kimi.contract.js";
|
|
|
44
46
|
export { logsContract } from "./contracts/logs.contract.js";
|
|
45
47
|
export { panelsContract } from "./contracts/panels.contract.js";
|
|
46
48
|
export { portsContract } from "./contracts/ports.contract.js";
|
|
49
|
+
export { pushContract } from "./contracts/push.contract.js";
|
|
47
50
|
export { secretsContract } from "./contracts/secrets.contract.js";
|
|
48
51
|
export { sessionsContract } from "./contracts/sessions.contract.js";
|
|
49
52
|
export { settingsContract } from "./contracts/settings.contract.js";
|
|
50
53
|
export { systemContract } from "./contracts/system.contract.js";
|
|
51
54
|
export { translatorContract } from "./contracts/translator.contract.js";
|
|
55
|
+
export { usageContract } from "./contracts/usage.contract.js";
|
|
52
56
|
export { vpnContract } from "./contracts/vpn.contract.js";
|
|
53
57
|
export { workspaceContract } from "./contracts/workspace.contract.js";
|
|
54
58
|
export * from "./effects.js";
|
|
@@ -85,8 +89,10 @@ export const sandboxContract = {
|
|
|
85
89
|
logs: logsContract,
|
|
86
90
|
panels: panelsContract,
|
|
87
91
|
ports: portsContract,
|
|
92
|
+
push: pushContract,
|
|
88
93
|
secrets: secretsContract,
|
|
89
94
|
system: systemContract,
|
|
90
95
|
translator: translatorContract,
|
|
96
|
+
usage: usageContract,
|
|
91
97
|
vpn: vpnContract,
|
|
92
98
|
};
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { expect, test } from "vitest";
|
|
2
|
+
import { SandboxSettingsSchema } from "./schemas.js";
|
|
3
|
+
|
|
4
|
+
/* The settings shape spans a version seam that really moves: the browser ships with the platform, the daemon
|
|
5
|
+
* ships inside the user's sandbox image, so a web build routinely parses a payload from an OLDER daemon. These
|
|
6
|
+
* tests pin the property that makes that survivable — an absent key is that flag's default, not a parse
|
|
7
|
+
* failure — because failing instead reaches the user as a settings page whose switches silently do nothing. */
|
|
8
|
+
|
|
9
|
+
test("a payload from a build that predates a toggle parses, with the new toggle at its default", () => {
|
|
10
|
+
// What a daemon built before the output-cleaner backend switch answers with: every key it knew, and
|
|
11
|
+
// nothing for the one added after it shipped.
|
|
12
|
+
const older = {
|
|
13
|
+
stableSystemPrompt: false,
|
|
14
|
+
skills: [],
|
|
15
|
+
hashlineEdits: false,
|
|
16
|
+
terseOutput: true,
|
|
17
|
+
iqSearch: true,
|
|
18
|
+
outputCleaners: "-cap",
|
|
19
|
+
outputHoldout: 0.1,
|
|
20
|
+
};
|
|
21
|
+
expect(SandboxSettingsSchema.parse(older)).toEqual({ ...older, filterBackend: "native" });
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
test("an empty object is the full default settings object", () => {
|
|
25
|
+
expect(SandboxSettingsSchema.parse({})).toEqual({
|
|
26
|
+
stableSystemPrompt: false,
|
|
27
|
+
skills: [],
|
|
28
|
+
hashlineEdits: false,
|
|
29
|
+
terseOutput: false,
|
|
30
|
+
iqSearch: false,
|
|
31
|
+
outputCleaners: "off",
|
|
32
|
+
outputHoldout: 0,
|
|
33
|
+
filterBackend: "native",
|
|
34
|
+
});
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
test("a key of the wrong type is still a parse failure — tolerance is for absence, not for garbage", () => {
|
|
38
|
+
expect(SandboxSettingsSchema.safeParse({ iqSearch: "yes" }).success).toBe(false);
|
|
39
|
+
expect(SandboxSettingsSchema.safeParse({ outputHoldout: 4 }).success).toBe(false);
|
|
40
|
+
});
|
package/src/schemas.ts
CHANGED
|
@@ -222,9 +222,11 @@ export type TranslatorAccounts = z.infer<typeof TranslatorAccountsSchema>;
|
|
|
222
222
|
// clarifying questions, a per-tool permission prompt — parks on the SAME registry keyed by `requestId`, so
|
|
223
223
|
// one route resolves all three; the `kind` says which card answered and carries its payload.
|
|
224
224
|
export const AgentReplySchema = z.discriminatedUnion("kind", [
|
|
225
|
-
// ExitPlanMode approval. `mode` is the posture to execute the approved plan in —
|
|
226
|
-
//
|
|
227
|
-
// as a session setMode.
|
|
225
|
+
// ExitPlanMode approval. `mode` is the posture to execute the approved plan in — auto-accept edits
|
|
226
|
+
// (acceptEdits), approve each one (default), or run everything (bypassPermissions); it rides back to the SDK
|
|
227
|
+
// as a session setMode. Absent, the turn returns to the posture it STARTED in, so an agent that put itself
|
|
228
|
+
// into plan mode does not cost the user the permissions they granted. Rejection feedback loops back into the
|
|
229
|
+
// model as the denial reason.
|
|
228
230
|
z.object({
|
|
229
231
|
kind: z.literal("plan"),
|
|
230
232
|
requestId: z.string().min(1),
|
|
@@ -240,8 +242,9 @@ export const AgentReplySchema = z.discriminatedUnion("kind", [
|
|
|
240
242
|
answers: z.record(z.string(), z.array(z.string())).optional(),
|
|
241
243
|
cancelled: z.boolean().optional(),
|
|
242
244
|
}),
|
|
243
|
-
// A per-tool permission prompt. 'once' allows this call only; 'always'
|
|
244
|
-
//
|
|
245
|
+
// A per-tool permission prompt. 'once' allows this call only; 'always' allows the whole TOOL for the rest
|
|
246
|
+
// of the session (plus the SDK's own narrower suggestions), which is what the card's label promises;
|
|
247
|
+
// 'deny' blocks it and feeds `feedback` back as the reason.
|
|
245
248
|
z.object({
|
|
246
249
|
kind: z.literal("permission"),
|
|
247
250
|
requestId: z.string().min(1),
|
|
@@ -402,16 +405,23 @@ export const SessionTranscriptSchema = z.object({ messages: z.array(SessionTrans
|
|
|
402
405
|
// A/B backend switch, so native and rtk can be benchmarked head-to-head.
|
|
403
406
|
// The booleans default off, skills defaults [] (no skill loaded), outputCleaners defaults "off" (cleaning off),
|
|
404
407
|
// outputHoldout 0, filterBackend "native" — a fresh sandbox starts with cleaning and iq off until the owner enables them.
|
|
405
|
-
|
|
408
|
+
//
|
|
409
|
+
// Every field carries that default IN THE SCHEMA, so a settings object written before a field existed still
|
|
410
|
+
// parses — the absent key reads as its default. That is not a compatibility layer, it is the seam this shape
|
|
411
|
+
// spans: the browser ships with the platform while the daemon ships inside the user's sandbox image, so a web
|
|
412
|
+
// build is routinely NEWER than the daemon answering it. Requiring the key instead makes the whole settings
|
|
413
|
+
// surface fail to parse the moment a toggle is added — which reaches the user as a page of switches that are
|
|
414
|
+
// silently dead, not as an error. It also means an older on-disk manifest keeps the owner's other picks rather
|
|
415
|
+
// than being discarded whole.
|
|
406
416
|
export const SandboxSettingsSchema = z.object({
|
|
407
|
-
stableSystemPrompt: z.boolean(),
|
|
408
|
-
skills: z.array(z.string()),
|
|
409
|
-
hashlineEdits: z.boolean(),
|
|
410
|
-
terseOutput: z.boolean(),
|
|
411
|
-
iqSearch: z.boolean(),
|
|
412
|
-
outputCleaners: z.string(),
|
|
413
|
-
outputHoldout: z.number().min(0).max(1),
|
|
414
|
-
filterBackend: z.enum(["native", "rtk"]),
|
|
417
|
+
stableSystemPrompt: z.boolean().default(false),
|
|
418
|
+
skills: z.array(z.string()).default([]),
|
|
419
|
+
hashlineEdits: z.boolean().default(false),
|
|
420
|
+
terseOutput: z.boolean().default(false),
|
|
421
|
+
iqSearch: z.boolean().default(false),
|
|
422
|
+
outputCleaners: z.string().default("off"),
|
|
423
|
+
outputHoldout: z.number().min(0).max(1).default(0),
|
|
424
|
+
filterBackend: z.enum(["native", "rtk"]).default("native"),
|
|
415
425
|
});
|
|
416
426
|
export type SandboxSettings = z.infer<typeof SandboxSettingsSchema>;
|
|
417
427
|
|
|
@@ -437,19 +447,40 @@ export const IntenticRunSchema = z.object({ args: z.array(z.string()) });
|
|
|
437
447
|
|
|
438
448
|
// ---- git ----
|
|
439
449
|
|
|
450
|
+
// What a commit records — two shapes, each a real git spelling:
|
|
451
|
+
// all: true ⇒ stage every change in the repo, then commit (`commit -a`; VSCode's "stage all and commit")
|
|
452
|
+
// absent ⇒ commit whatever is staged (plain `git commit`)
|
|
453
|
+
//
|
|
454
|
+
// There is deliberately no `paths`. The index IS git's mechanism for choosing what a commit contains, so a
|
|
455
|
+
// second path-selection channel alongside it can only disagree with it: a `commit --only` over a partially
|
|
456
|
+
// staged file records the WORKTREE content while the row the user picked showed the INDEX content. Staging is
|
|
457
|
+
// the selection; this endpoint only ever records it.
|
|
440
458
|
export const CommitSchema = RepoParamSchema.extend({
|
|
441
459
|
message: z.string().min(1),
|
|
442
|
-
|
|
443
|
-
paths: z.array(z.string().min(1)).max(500).optional(),
|
|
460
|
+
all: z.boolean().optional(),
|
|
444
461
|
});
|
|
445
462
|
export const DiscardSchema = RepoParamSchema.extend({
|
|
446
463
|
// Repo-relative paths to discard; absent ⇒ discard every uncommitted change in the repo.
|
|
447
464
|
paths: z.array(z.string().min(1)).max(500).optional(),
|
|
448
465
|
});
|
|
449
|
-
|
|
466
|
+
// Index moves. Both are per-path and never touch the worktree, so they are always safe and need no checkpoint.
|
|
467
|
+
export const GitStageSchema = RepoParamSchema.extend({ paths: z.array(z.string().min(1)).max(500) });
|
|
468
|
+
// `branch` defaults to the checked-out one. There is deliberately no "set upstream" flag: the daemon publishes
|
|
469
|
+
// (`push -u`) exactly when the branch has no upstream yet, which is never destructive and is the only way the
|
|
470
|
+
// result is coherent — see pushBranch.
|
|
471
|
+
export const PushSchema = RepoParamSchema.extend({ branch: z.string().min(1).optional() });
|
|
450
472
|
export const GitFileQuerySchema = RepoParamSchema.extend({ path: z.string().min(1) });
|
|
451
473
|
export const GitFileWriteSchema = RepoParamSchema.extend({ path: z.string().min(1), content: z.string() });
|
|
452
|
-
|
|
474
|
+
// Which of the working tree's diffs to open — the same split the Changes panel lists under. A path that is
|
|
475
|
+
// staged AND edited again has genuinely different diffs, so the side is required rather than defaulted: a
|
|
476
|
+
// caller that doesn't say which one it means doesn't know what it is showing.
|
|
477
|
+
// staged ⇒ index vs HEAD (what a bare `git commit` would record)
|
|
478
|
+
// unstaged ⇒ worktree vs index (untracked ⇒ no before side)
|
|
479
|
+
// conflicted ⇒ HEAD vs worktree (what you had vs what the merge left, markers included — an unmerged path
|
|
480
|
+
// has no stage 0, so the index is not a side it can be diffed against)
|
|
481
|
+
export const GitDiffSideSchema = z.enum(["staged", "unstaged", "conflicted"]);
|
|
482
|
+
export type GitDiffSide = z.infer<typeof GitDiffSideSchema>;
|
|
483
|
+
export const GitFileDiffQuerySchema = RepoParamSchema.extend({ path: z.string().min(1), side: GitDiffSideSchema });
|
|
453
484
|
export const GitStatusSchema = z.object({ branch: z.string(), dirty: z.boolean(), files: z.array(z.string()) });
|
|
454
485
|
export const GitFilesSchema = z.object({ files: z.array(z.string()) });
|
|
455
486
|
export const GitFileSchema = z.object({ path: z.string(), content: z.string() });
|
|
@@ -460,29 +491,120 @@ export const CommitResultSchema = z.object({ committed: z.boolean() });
|
|
|
460
491
|
export const GitChangeSchema = z.object({
|
|
461
492
|
// Repo-relative path with forward slashes; for "renamed" the NEW path (`from` carries the old one).
|
|
462
493
|
path: z.string(),
|
|
463
|
-
|
|
494
|
+
// "conflicted" is git's unmerged state (`U`), and it is not a kind of modification: the index holds "ours"
|
|
495
|
+
// and "theirs" at stages 2/3 with NO stage 0, so there is nothing a commit could record for this path and
|
|
496
|
+
// git refuses to commit while one exists. It belongs to neither side — see RepoChanges.conflicted.
|
|
497
|
+
status: z.enum(["added", "modified", "deleted", "renamed", "type-changed", "conflicted"]),
|
|
464
498
|
from: z.string().optional(),
|
|
465
499
|
additions: z.number().optional(),
|
|
466
500
|
deletions: z.number().optional(),
|
|
467
501
|
});
|
|
468
502
|
export type GitChange = z.infer<typeof GitChangeSchema>;
|
|
503
|
+
|
|
504
|
+
// Where a repo's checked-out branch stands against its remote. Every field is optional-or-zero because every
|
|
505
|
+
// one of them is legitimately absent in a healthy repo: no remote configured yet, a branch created locally and
|
|
506
|
+
// never pushed, a detached HEAD. `ahead` = commits only we have; `behind` = commits only the upstream has,
|
|
507
|
+
// which is meaningful only as of the last fetch — the panel's Fetch button is what refreshes it.
|
|
508
|
+
export const GitRemoteStateSchema = z.object({
|
|
509
|
+
// The remote this branch pushes to: its OWN remote when it tracks one, else the first `git remote` lists
|
|
510
|
+
// (where a never-pushed branch would publish). Those differ in a fork — `origin` and `upstream` both
|
|
511
|
+
// configured — and pushing to the wrong one succeeds while leaving `ahead` stuck. Absent ⇒ no remote.
|
|
512
|
+
remote: z.string().optional(),
|
|
513
|
+
// The checked-out branch; absent on a detached HEAD or an unborn repo.
|
|
514
|
+
branch: z.string().optional(),
|
|
515
|
+
// The tracking ref ("origin/main"); absent ⇒ this branch has no upstream, so the next push publishes it.
|
|
516
|
+
upstream: z.string().optional(),
|
|
517
|
+
ahead: z.number(),
|
|
518
|
+
behind: z.number(),
|
|
519
|
+
});
|
|
520
|
+
export type GitRemoteState = z.infer<typeof GitRemoteStateSchema>;
|
|
521
|
+
|
|
522
|
+
// A ref name (branch/tag), validated structurally — git enforces the rest of ref-name legality itself.
|
|
523
|
+
const RefNameSchema = z
|
|
524
|
+
.string()
|
|
525
|
+
.regex(/^[A-Za-z0-9][A-Za-z0-9._/-]*$/)
|
|
526
|
+
.max(200);
|
|
527
|
+
|
|
528
|
+
// One local branch, for the switcher. `at` is its tip's committer time in ms (the list sorts newest-first).
|
|
529
|
+
export const GitBranchSchema = z.object({
|
|
530
|
+
name: z.string(),
|
|
531
|
+
current: z.boolean(),
|
|
532
|
+
upstream: z.string().optional(),
|
|
533
|
+
ahead: z.number(),
|
|
534
|
+
behind: z.number(),
|
|
535
|
+
// The configured upstream no longer exists on the remote (a merged PR's deleted branch) — distinct from
|
|
536
|
+
// "no upstream", and the signal that this local branch is safe to delete.
|
|
537
|
+
gone: z.boolean().optional(),
|
|
538
|
+
at: z.number(),
|
|
539
|
+
});
|
|
540
|
+
export type GitBranch = z.infer<typeof GitBranchSchema>;
|
|
541
|
+
export const GitBranchesSchema = z.object({ branches: z.array(GitBranchSchema) });
|
|
542
|
+
// Create at `start` (a sha or ref; absent ⇒ HEAD); `checkout` switches to it immediately (`git switch -c`).
|
|
543
|
+
export const GitBranchCreateAtSchema = RepoParamSchema.extend({
|
|
544
|
+
name: RefNameSchema,
|
|
545
|
+
start: z.string().min(1).optional(),
|
|
546
|
+
checkout: z.boolean().optional(),
|
|
547
|
+
});
|
|
548
|
+
// `force` is the deliberate retry after git refuses to drop an unmerged branch.
|
|
549
|
+
export const GitBranchDeleteSchema = RepoParamSchema.extend({ name: RefNameSchema, force: z.boolean().optional() });
|
|
550
|
+
|
|
469
551
|
export const RepoChangesSchema = z.object({
|
|
470
552
|
// The {repo} param the per-repo git routes accept: "root" or a repo id (its root-relative dir).
|
|
471
553
|
repo: z.string(),
|
|
472
554
|
// Absent on an unborn HEAD (a repo initialized but never committed).
|
|
473
555
|
branch: z.string().optional(),
|
|
474
|
-
|
|
556
|
+
// Unmerged paths — a merge, rebase, cherry-pick or pull that git could not finish. First, because until
|
|
557
|
+
// they are resolved nothing else in this repo can be committed at all: git refuses outright. Held apart
|
|
558
|
+
// from the two sides rather than listed in them, because "staged or not" is not a question an unmerged path
|
|
559
|
+
// has an answer to. Staging one (`git add`) is exactly how you tell git it is resolved.
|
|
560
|
+
conflicted: z.array(GitChangeSchema),
|
|
561
|
+
// The two sides git actually models, kept apart because a path can appear on BOTH with different statuses
|
|
562
|
+
// (a staged edit that was then edited again — the classic `MM`). `staged` is index-vs-HEAD: exactly what a
|
|
563
|
+
// bare `git commit` would record. `unstaged` is worktree-vs-index plus untracked files. Each side's
|
|
564
|
+
// additions/deletions describe the diff it is listed under, never a conflation of the two.
|
|
565
|
+
staged: z.array(GitChangeSchema),
|
|
566
|
+
unstaged: z.array(GitChangeSchema),
|
|
567
|
+
// Where this repo stands against its remote; `ahead`/`behind` are 0 with no remote or no upstream.
|
|
568
|
+
remote: GitRemoteStateSchema.optional(),
|
|
569
|
+
// WHICH AGENT PUT IT THERE: repo-relative path → the agent ids that landed it, newest land first. Keyed by
|
|
570
|
+
// PATH rather than carried on each GitChange because a path can be listed on two sides at once (staged and
|
|
571
|
+
// edited again) and its origin is the same fact for both. Only agents can appear here — a main-tree turn,
|
|
572
|
+
// a terminal edit and your own typing never pass through land, so they are simply absent (see
|
|
573
|
+
// agents/origins.ts), which is why the panel badges an agent and says nothing at all for anyone else.
|
|
574
|
+
// Ids, not titles: every client already mirrors the fleet registry and can resolve one to the other.
|
|
575
|
+
origins: z.record(z.string(), z.array(z.string())).optional(),
|
|
475
576
|
// Why the repo could not be scanned at all, condensed to git's own one-line reason ("fatal: bad object HEAD").
|
|
476
577
|
// A repo left torn by a canceled or failed upload used to be dropped from the response entirely, so it just
|
|
477
|
-
// vanished from the panel with nothing to act on; it now arrives with empty
|
|
578
|
+
// vanished from the panel with nothing to act on; it now arrives with empty change lists and this set instead.
|
|
478
579
|
error: z.string().optional(),
|
|
479
580
|
});
|
|
480
581
|
export type RepoChanges = z.infer<typeof RepoChangesSchema>;
|
|
481
|
-
// The aggregated review set across every repo (root + every discovered repo); a repo appears when it has changes
|
|
482
|
-
// or when it failed to scan.
|
|
582
|
+
// The aggregated review set across every repo (root + every discovered repo); a repo appears when it has changes,
|
|
583
|
+
// when it is out of sync with its remote, or when it failed to scan.
|
|
483
584
|
export const GitChangesSchema = z.object({ repos: z.array(RepoChangesSchema) });
|
|
484
585
|
export type GitChanges = z.infer<typeof GitChangesSchema>;
|
|
485
586
|
|
|
587
|
+
// One file an agent touched, plus whether that change is ALREADY in the main tree. The review lists the
|
|
588
|
+
// agent's CUMULATIVE output (base → worktree), not just the not-yet-landed remainder, because landing is not
|
|
589
|
+
// the end of the review: a clean turn auto-lands within milliseconds, and a list scoped to the remainder shows
|
|
590
|
+
// the user an empty panel for work they never got to look at. `landed` is what still separates the two — the
|
|
591
|
+
// remainder is what "Land now" would apply, and the panel filters on exactly this flag.
|
|
592
|
+
export const AgentChangeSchema = GitChangeSchema.extend({ landed: z.boolean() });
|
|
593
|
+
export type AgentChange = z.infer<typeof AgentChangeSchema>;
|
|
594
|
+
|
|
595
|
+
// An agent conversation-worktree's delta vs its recorded base — deliberately NOT RepoChanges. There is no index
|
|
596
|
+
// side to speak of here: the question a fleet review answers is "what did this agent write", which is one flat
|
|
597
|
+
// set. Sharing the working-tree shape would have forced a meaningless empty `staged` on every
|
|
598
|
+
// row and invited the panel to render a staging affordance that cannot work on a worktree the user never checks out.
|
|
599
|
+
export const AgentRepoChangesSchema = z.object({
|
|
600
|
+
repo: z.string(),
|
|
601
|
+
branch: z.string().optional(),
|
|
602
|
+
changes: z.array(AgentChangeSchema),
|
|
603
|
+
});
|
|
604
|
+
export type AgentRepoChanges = z.infer<typeof AgentRepoChangesSchema>;
|
|
605
|
+
export const AgentChangesSchema = z.object({ repos: z.array(AgentRepoChangesSchema) });
|
|
606
|
+
export type AgentChanges = z.infer<typeof AgentChangesSchema>;
|
|
607
|
+
|
|
486
608
|
// ---- git history graph (the "Git Graph" view over a repo's real commits) ----
|
|
487
609
|
// A hex sha (full or git-abbreviated): the only shape the graph ever sends back, so the per-commit routes
|
|
488
610
|
// constrain to it rather than accepting an arbitrary git revision expression.
|
|
@@ -524,11 +646,8 @@ export const GitCommitFileDiffQuerySchema = RepoParamSchema.extend({ sha: ShaSch
|
|
|
524
646
|
// merge / rebase / drop) add or replay commits and are auto-checkpointed daemon-side; a conflict aborts and
|
|
525
647
|
// reports `ok:false` (an expected outcome, not a throw). Checkout and reset move HEAD (reset --hard discards
|
|
526
648
|
// the worktree) — also auto-checkpointed. A `{repo, sha}` names the target commit for every commit-scoped
|
|
527
|
-
// action; a ref name (branch/tag) is validated structurally, git enforces the rest of ref-name legality
|
|
528
|
-
|
|
529
|
-
.string()
|
|
530
|
-
.regex(/^[A-Za-z0-9][A-Za-z0-9._/-]*$/)
|
|
531
|
-
.max(200);
|
|
649
|
+
// action; a ref name (branch/tag) is validated structurally, git enforces the rest of ref-name legality
|
|
650
|
+
// (RefNameSchema is declared above, with the branch schemas that first use it).
|
|
532
651
|
export const GitBranchCreateSchema = RepoParamSchema.extend({ sha: ShaSchema, name: RefNameSchema });
|
|
533
652
|
export const GitTagCreateSchema = RepoParamSchema.extend({ sha: ShaSchema, name: RefNameSchema });
|
|
534
653
|
export const GitCheckoutSchema = RepoParamSchema.extend({ ref: RefNameSchema });
|
|
@@ -958,6 +1077,21 @@ export type VpnProvider = z.infer<typeof VpnProviderSchema>;
|
|
|
958
1077
|
|
|
959
1078
|
const autoConnect = z.enum(["on", "off"]).default("on");
|
|
960
1079
|
|
|
1080
|
+
// FortiClient wraps every stored credential in its own "EncX <hex>" (older builds: "Enc <hex>") encryption,
|
|
1081
|
+
// keyed to the machine that exported the config — it is NOT recoverable from the file. Pasting one is an easy
|
|
1082
|
+
// mistake to make, because in the XML it sits exactly where the credential belongs, and the failure it causes
|
|
1083
|
+
// is unreadable: phase 1 negotiates fine and IKE then reports "calculated HASH does not match HASH payload",
|
|
1084
|
+
// which says nothing about where the bad value came from. Rejecting it here turns that into a sentence at the
|
|
1085
|
+
// point of entry. (The FortiClient importer already drops these — this catches a hand-paste.)
|
|
1086
|
+
// Exported so the add form can flag it inline on blur instead of only on a rejected round-trip — one
|
|
1087
|
+
// definition of what "this is ciphertext, not a credential" means, shared by the browser and the daemon.
|
|
1088
|
+
export const isForticlientCiphertext = (value: string): boolean => /^Enc[X]?\s+[0-9A-Fa-f]{8,}$/.test(value.trim());
|
|
1089
|
+
|
|
1090
|
+
const notForticlientCiphertext = <T extends z.ZodType<string>>(field: T, label: string): T =>
|
|
1091
|
+
field.refine((value) => !isForticlientCiphertext(value), {
|
|
1092
|
+
message: `That looks like a value copied straight out of a FortiClient config — FortiClient encrypts it with a key tied to the machine that exported it, so it can't be used here. Enter the actual ${label} (ask whoever administers the gateway).`,
|
|
1093
|
+
}) as unknown as T;
|
|
1094
|
+
|
|
961
1095
|
export const WireguardVpnConfigSchema = z.object({
|
|
962
1096
|
provider: z.literal("wireguard"),
|
|
963
1097
|
// The pasted .conf ([Interface] + [Peer]) — it holds the private key, so it's this arm's secret field.
|
|
@@ -970,7 +1104,7 @@ export const FortinetVpnConfigSchema = z.object({
|
|
|
970
1104
|
server: z.string().min(1),
|
|
971
1105
|
port: z.coerce.number().int().min(1).max(65535).default(443),
|
|
972
1106
|
username: z.string().min(1),
|
|
973
|
-
password: z.string().min(1),
|
|
1107
|
+
password: notForticlientCiphertext(z.string().min(1), "password"),
|
|
974
1108
|
// A FortiGate on a self-signed/private-CA certificate: openconnect pins this digest
|
|
975
1109
|
// ("sha256:…", copied from its own refusal message) instead of trusting a CA. Absent ⇒ normal CA validation.
|
|
976
1110
|
trustedCert: z.string().min(1).optional(),
|
|
@@ -981,14 +1115,25 @@ export const FortinetVpnConfigSchema = z.object({
|
|
|
981
1115
|
export const IpsecVpnConfigSchema = z.object({
|
|
982
1116
|
provider: z.literal("ipsec"),
|
|
983
1117
|
server: z.string().min(1),
|
|
984
|
-
presharedKey: z.string().min(1),
|
|
1118
|
+
presharedKey: notForticlientCiphertext(z.string().min(1), "pre-shared key"),
|
|
985
1119
|
// The local IKE identity (FortiClient's <localid>) — dial-up FortiGates key their phase-1 selection off it.
|
|
986
1120
|
localId: z.string().min(1).optional(),
|
|
987
1121
|
remoteId: z.string().min(1).optional(),
|
|
988
1122
|
// XAuth (FortiClient's <xauth>) — absent for PSK-only tunnels.
|
|
989
1123
|
username: z.string().min(1).optional(),
|
|
990
|
-
password: z.string().min(1).optional(),
|
|
1124
|
+
password: notForticlientCiphertext(z.string().min(1), "XAuth password").optional(),
|
|
991
1125
|
ikeVersion: z.enum(["1", "2"]).default("1"),
|
|
1126
|
+
// Perfect Forward Secrecy for phase 2. Must match the gateway EXACTLY: it decides whether a KE payload is
|
|
1127
|
+
// sent in quick mode, and a mismatch fails with NO_PROPOSAL_CHOSEN only after phase 1 and XAuth have
|
|
1128
|
+
// succeeded — which reads like anything but a phase 2 problem. FortiClient stores it as <pfs> under
|
|
1129
|
+
// <ipsec_settings> and defaults it on, so that is the default here too.
|
|
1130
|
+
pfs: z.enum(["on", "off"]).default("on"),
|
|
1131
|
+
// The Diffie-Hellman group, as FortiClient numbers them. ONE field for both phases on purpose: in IKEv1
|
|
1132
|
+
// strongSwan sends a single KE payload in quick mode and the phase-2 group ends up following phase 1, so
|
|
1133
|
+
// offering a phase-1 list that starts with a different group than the gateway wants for phase 2 fails with
|
|
1134
|
+
// NO_PROPOSAL_CHOSEN no matter what the esp= line says. 14 (modp2048) is FortiClient's phase-2 default;
|
|
1135
|
+
// it is <dhgroup> under <ipsec_settings> in an export.
|
|
1136
|
+
dhGroup: z.enum(["2", "5", "14", "15", "16", "19", "20"]).default("14"),
|
|
992
1137
|
// IKEv1 aggressive mode: insecure by construction, and exactly what FortiGate dial-up with a group PSK
|
|
993
1138
|
// requires — hence opt-in per connection rather than a global strongSwan setting.
|
|
994
1139
|
aggressive: z.enum(["on", "off"]).default("on"),
|
|
@@ -1140,6 +1285,9 @@ export const ForticlientConnectionSchema = z.object({
|
|
|
1140
1285
|
// ipsec-only, and only when the file stored them in the clear.
|
|
1141
1286
|
localId: z.string().optional(),
|
|
1142
1287
|
aggressive: z.boolean().optional(),
|
|
1288
|
+
// Phase-2 settings, read from <ipsec_settings> — the pair that decides whether quick mode can succeed.
|
|
1289
|
+
pfs: z.boolean().optional(),
|
|
1290
|
+
dhGroup: z.string().optional(),
|
|
1143
1291
|
// What the user still has to supply for this connection to dial (always at least the password).
|
|
1144
1292
|
needs: z.array(z.string()),
|
|
1145
1293
|
});
|
|
@@ -1576,9 +1724,75 @@ export const ActivityStatusSchema = z.object({
|
|
|
1576
1724
|
});
|
|
1577
1725
|
export type ActivityStatus = z.infer<typeof ActivityStatusSchema>;
|
|
1578
1726
|
|
|
1727
|
+
// ---- usage: the durable spend ledger ----
|
|
1728
|
+
// One row per attributed turn, appended at turn end and NEVER pruned. This exists because the activity log
|
|
1729
|
+
// can't answer a money question: it prunes to its most recent entries, so a month's spend is unanswerable and
|
|
1730
|
+
// — worse for a cost readout — the totals SHRINK as newer turns evict older ones. The ledger keeps the raw
|
|
1731
|
+
// per-turn facts and the rollup projects them on read, so a new grouping (by day, by model, by conversation)
|
|
1732
|
+
// needs no new storage and no migration.
|
|
1733
|
+
export const UsageTurnSchema = z.object({
|
|
1734
|
+
// Epoch ms at turn end. Kept alongside `day` so a future timezone-aware rollup is a pure change over data
|
|
1735
|
+
// already on disk.
|
|
1736
|
+
at: z.number(),
|
|
1737
|
+
// The UTC calendar day (YYYY-MM-DD) `at` fell in — precomputed so a rollup never re-derives a timezone.
|
|
1738
|
+
day: z.string(),
|
|
1739
|
+
provider: z.string(),
|
|
1740
|
+
// Absent on an env-token turn, which has no account to attribute to (same rule as the activity log).
|
|
1741
|
+
account: z.string().optional(),
|
|
1742
|
+
// The model the turn ACTUALLY ran, resolved past the client's pick and every provider default. Absent only
|
|
1743
|
+
// when the provider's own subscription default served it without the daemon naming one.
|
|
1744
|
+
model: z.string().optional(),
|
|
1745
|
+
harness: z.string(),
|
|
1746
|
+
// The conversation this turn belonged to, so spend can join to a fleet agent. Absent on a main-tree turn.
|
|
1747
|
+
conversationId: z.string().optional(),
|
|
1748
|
+
// The provider's own turn count for the request (a Claude "turn" can be several under the hood), so turns
|
|
1749
|
+
// and cost stay comparable across providers. 1 when the provider reported none.
|
|
1750
|
+
turns: z.number(),
|
|
1751
|
+
inputTokens: z.number(),
|
|
1752
|
+
outputTokens: z.number(),
|
|
1753
|
+
cacheReadTokens: z.number(),
|
|
1754
|
+
cacheCreationTokens: z.number(),
|
|
1755
|
+
costUsd: z.number(),
|
|
1756
|
+
durationMs: z.number(),
|
|
1757
|
+
});
|
|
1758
|
+
export type UsageTurn = z.infer<typeof UsageTurnSchema>;
|
|
1759
|
+
|
|
1760
|
+
// The ledger grouped by day × provider × account × model × harness × conversation — the finest grouping any
|
|
1761
|
+
// dashboard panel needs, and a handful of rows per active day instead of one per turn, so a year of history is
|
|
1762
|
+
// well under a MB over the tunnel. Every panel (spend per day, cost by model, cost by agent, cache hit rate) is
|
|
1763
|
+
// a projection of these.
|
|
1764
|
+
// The conversation is in the KEY, not merely along for the ride, because cost-by-agent has to answer within the
|
|
1765
|
+
// same window as every other panel on the screen. The fleet registry also carries a per-agent total, but only a
|
|
1766
|
+
// cumulative, all-time one — reading it beside a "last 7 days" filter would print an all-time number under a
|
|
1767
|
+
// windowed heading, which is the shrinking-totals bug wearing a different hat.
|
|
1768
|
+
export const UsageRollupRowSchema = z.object({
|
|
1769
|
+
day: z.string(),
|
|
1770
|
+
provider: z.string(),
|
|
1771
|
+
account: z.string().optional(),
|
|
1772
|
+
model: z.string().optional(),
|
|
1773
|
+
harness: z.string(),
|
|
1774
|
+
conversationId: z.string().optional(),
|
|
1775
|
+
turns: z.number(),
|
|
1776
|
+
inputTokens: z.number(),
|
|
1777
|
+
outputTokens: z.number(),
|
|
1778
|
+
cacheReadTokens: z.number(),
|
|
1779
|
+
cacheCreationTokens: z.number(),
|
|
1780
|
+
costUsd: z.number(),
|
|
1781
|
+
durationMs: z.number(),
|
|
1782
|
+
});
|
|
1783
|
+
export type UsageRollupRow = z.infer<typeof UsageRollupRowSchema>;
|
|
1784
|
+
// Inclusive UTC day bounds (YYYY-MM-DD). Both absent ⇒ the whole ledger.
|
|
1785
|
+
export const UsageRollupQuerySchema = z.object({
|
|
1786
|
+
from: z.string().optional(),
|
|
1787
|
+
to: z.string().optional(),
|
|
1788
|
+
});
|
|
1789
|
+
export type UsageRollupQuery = z.infer<typeof UsageRollupQuerySchema>;
|
|
1790
|
+
export const UsageRollupSchema = z.object({ rows: z.array(UsageRollupRowSchema) });
|
|
1791
|
+
|
|
1579
1792
|
// ---- usage: per-account token/cost totals ----
|
|
1580
|
-
//
|
|
1581
|
-
//
|
|
1793
|
+
// The account picker's headroom readout, folded from the ledger above (all-time, not a log window), grouped by
|
|
1794
|
+
// provider+account. `account` is the attribution key, so env-token turns are excluded rather than pooled under
|
|
1795
|
+
// a blank id — an unattributed turn belongs to no account's total.
|
|
1582
1796
|
export const UsageAccountSchema = z.object({
|
|
1583
1797
|
provider: z.string(),
|
|
1584
1798
|
account: z.string(),
|
|
@@ -1631,3 +1845,43 @@ export const PresenceReportSchema = z.object({
|
|
|
1631
1845
|
path: z.string().optional(),
|
|
1632
1846
|
});
|
|
1633
1847
|
export type PresenceReport = z.infer<typeof PresenceReportSchema>;
|
|
1848
|
+
|
|
1849
|
+
// ---- push: web-push notifications to the owner's devices ----
|
|
1850
|
+
// The daemon is the only tier that knows what the agent is doing, so it is the sender. Subscriptions are
|
|
1851
|
+
// per-BROWSER (the endpoint is minted by that browser's push service — Google's, Mozilla's, Apple's), which
|
|
1852
|
+
// is why they live here and not on the platform: the platform is off the command path and would have to be
|
|
1853
|
+
// told about every turn to be useful.
|
|
1854
|
+
|
|
1855
|
+
// A browser's PushSubscription, in the exact shape `web-push` consumes — the browser produces it via
|
|
1856
|
+
// PushManager.subscribe() and the client posts it back verbatim, so the daemon never reshapes it.
|
|
1857
|
+
export const PushSubscriptionSchema = z.object({
|
|
1858
|
+
endpoint: z.string().url(),
|
|
1859
|
+
keys: z.object({
|
|
1860
|
+
// The client's public key and auth secret for payload encryption (RFC 8291). Opaque base64url here.
|
|
1861
|
+
p256dh: z.string().min(1),
|
|
1862
|
+
auth: z.string().min(1),
|
|
1863
|
+
}),
|
|
1864
|
+
});
|
|
1865
|
+
export type PushSubscription = z.infer<typeof PushSubscriptionSchema>;
|
|
1866
|
+
|
|
1867
|
+
// What the service worker renders. `url` is the in-app route the notification opens (the click handler
|
|
1868
|
+
// focuses an existing tab there rather than spawning a new one); `tag` collapses repeats — a second
|
|
1869
|
+
// "waiting on you" for the same conversation REPLACES the first instead of stacking.
|
|
1870
|
+
export const PushNotificationSchema = z.object({
|
|
1871
|
+
title: z.string().min(1),
|
|
1872
|
+
body: z.string(),
|
|
1873
|
+
url: z.string().optional(),
|
|
1874
|
+
tag: z.string().optional(),
|
|
1875
|
+
// Whether the notification stays on screen until dismissed. Set for the "agent is blocked on you" cases,
|
|
1876
|
+
// where a notification that auto-dismisses is a request that silently went unanswered.
|
|
1877
|
+
requireInteraction: z.boolean().optional(),
|
|
1878
|
+
});
|
|
1879
|
+
export type PushNotification = z.infer<typeof PushNotificationSchema>;
|
|
1880
|
+
|
|
1881
|
+
// The VAPID public key a browser needs to subscribe, plus whether this browser's endpoint is already known —
|
|
1882
|
+
// so the settings toggle can render its true state instead of trusting the browser's permission alone (a
|
|
1883
|
+
// granted permission with no server-side row would notify nothing).
|
|
1884
|
+
export const PushConfigSchema = z.object({ publicKey: z.string(), subscribed: z.boolean() });
|
|
1885
|
+
export const PushEndpointSchema = z.object({ endpoint: z.string().url() });
|
|
1886
|
+
// `endpoint` identifies which browser is asking, so `subscribed` can be answered for THIS one.
|
|
1887
|
+
export const PushConfigQuerySchema = z.object({ endpoint: z.string().url().optional() });
|