@intentic/sandbox-contract 1.214.0 → 1.216.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/capability-secrets.d.ts +3 -0
- package/dist/capability-secrets.d.ts.map +1 -0
- package/dist/capability-secrets.js +3 -0
- package/dist/capability-secrets.js.map +1 -0
- package/dist/contracts/agent.contract.d.ts +10 -0
- package/dist/contracts/agent.contract.d.ts.map +1 -1
- package/dist/contracts/capabilities.contract.d.ts +1 -0
- package/dist/contracts/capabilities.contract.d.ts.map +1 -1
- package/dist/contracts/capabilities.contract.js.map +1 -1
- package/dist/contracts/git.contract.d.ts +21 -0
- package/dist/contracts/git.contract.d.ts.map +1 -1
- package/dist/contracts/git.contract.js +3 -1
- package/dist/contracts/git.contract.js.map +1 -1
- package/dist/contracts/host.contract.d.ts +1 -0
- package/dist/contracts/host.contract.d.ts.map +1 -1
- package/dist/contracts/settings.contract.d.ts +2 -85
- package/dist/contracts/settings.contract.d.ts.map +1 -1
- package/dist/contracts/system.contract.d.ts +6 -0
- package/dist/contracts/system.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 +1 -0
- package/dist/events.js.map +1 -1
- package/dist/index.d.ts +41 -85
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/dist/schemas.d.ts +50 -114
- package/dist/schemas.d.ts.map +1 -1
- package/dist/schemas.js +23 -10
- package/dist/schemas.js.map +1 -1
- package/dist/workspace-state.d.ts +1 -0
- package/dist/workspace-state.d.ts.map +1 -1
- package/dist/workspace-state.js +10 -1
- package/dist/workspace-state.js.map +1 -1
- package/package.json +4 -4
- package/src/capability-secrets.ts +20 -0
- package/src/contracts/capabilities.contract.ts +12 -5
- package/src/contracts/git.contract.ts +10 -0
- package/src/events.ts +23 -0
- package/src/index.ts +1 -0
- package/src/schemas.ts +159 -117
- package/src/workspace-state.test.ts +40 -0
- package/src/workspace-state.ts +29 -1
package/src/schemas.ts
CHANGED
|
@@ -1725,11 +1725,9 @@ export const SkillRemoveSchema = z.object({ name: SkillNameSchema });
|
|
|
1725
1725
|
// only. Opt-in (default off); the browser Search box uses iq regardless.
|
|
1726
1726
|
// iqSearchHoldout — conversation-level measurement control for iqSearch (UsageTurn.iqSearchArm). The arm
|
|
1727
1727
|
// stays fixed because teaching already loaded into a session cannot be removed next turn.
|
|
1728
|
-
//
|
|
1729
|
-
//
|
|
1730
|
-
//
|
|
1731
|
-
// answers ahead of it.
|
|
1732
|
-
// iqContextHoldout — measurement control for iqContext, same shape as terseHoldout (UsageTurn.iqContext).
|
|
1728
|
+
// workspaceMap — computes an AREA index of the project a run starts in and prepends it to the
|
|
1729
|
+
// conversation's opening message, so the turn does not have to buy its own orientation
|
|
1730
|
+
// with a directory listing. Generated from the filesystem every time, never stored.
|
|
1733
1731
|
// outputCleaners — the Bash output-cleaner spec (agent-output-filter): "off" = filter disabled (default),
|
|
1734
1732
|
// "" = all cleaners on, else an iq-style allow-list / default-minus
|
|
1735
1733
|
// spec ("git,pnpm" = only those; "-cap" = all except). Threaded to the filter via env.
|
|
@@ -1796,21 +1794,23 @@ export const SandboxSettingsSchema = z.object({
|
|
|
1796
1794
|
* a valid control here: once the teaching enters a provider session, withholding it from the next request
|
|
1797
1795
|
* does not make the model forget it. 0 ⇒ no measurement and every conversation receives the teaching. */
|
|
1798
1796
|
iqSearchHoldout: z.number().min(0).max(1).default(0),
|
|
1799
|
-
/*
|
|
1800
|
-
*
|
|
1801
|
-
* calls opens with the anchors already in hand. Independent of `iqSearch`, which only teaches the agent to
|
|
1802
|
-
* reach for the CLI once it decides to search — this one answers ahead of that decision, and the two
|
|
1803
|
-
* compose: the injected capsule names the anchors, the CLI is there for what it missed.
|
|
1797
|
+
/* THE MAP THE TURN OPENS WITH — which areas the project a run starts in has, one derived line on what each
|
|
1798
|
+
* is for, and where the run is standing among them (agent/workspace-map.ts).
|
|
1804
1799
|
*
|
|
1805
|
-
* It
|
|
1806
|
-
*
|
|
1807
|
-
*
|
|
1808
|
-
*
|
|
1809
|
-
|
|
1810
|
-
|
|
1811
|
-
|
|
1812
|
-
|
|
1813
|
-
|
|
1800
|
+
* It answers the question every first turn has whatever it was asked — "what is this and where am I in
|
|
1801
|
+
* it" — which across a hundred sessions of this workspace was being bought with a directory listing in two
|
|
1802
|
+
* turns out of five, and with ~5.3k tokens of tool results before the job was touched.
|
|
1803
|
+
*
|
|
1804
|
+
* ROOTED AT THE RUN'S STARTING FOLDER rather than at the workspace: a persona's start folder, an isolated
|
|
1805
|
+
* conversation's worktree, or wherever the turn's cwd is. It maps the project containing that folder and
|
|
1806
|
+
* names the rest of the workspace on one line, because a run three levels inside one project is not asking
|
|
1807
|
+
* about the others.
|
|
1808
|
+
*
|
|
1809
|
+
* REGENERATED, NEVER STORED, which is the whole reason it is a mechanism rather than a paragraph in the
|
|
1810
|
+
* system prompt or a hand-written CLAUDE.md: in the ten days that motivated it this repo's two busiest
|
|
1811
|
+
* top-level directories stopped existing, and every written-down copy of the layout was wrong by the end of
|
|
1812
|
+
* the window. Off by default — it spends its tokens on the opening message of every conversation. */
|
|
1813
|
+
workspaceMap: z.boolean().default(false),
|
|
1814
1814
|
outputCleaners: z.string().default("off"),
|
|
1815
1815
|
outputHoldout: z.number().min(0).max(1).default(0),
|
|
1816
1816
|
/* The models behind the small automatic jobs that are not a conversation — today the commit message
|
|
@@ -2037,34 +2037,17 @@ export type InputSavings = z.infer<typeof InputSavingsSchema>;
|
|
|
2037
2037
|
// without it. A mean PER TURN, because the arms never hold the same number of turns.
|
|
2038
2038
|
export const SavingsArmSchema = z.object({ turns: z.number(), mean: z.number() });
|
|
2039
2039
|
|
|
2040
|
-
/* WHAT BECAME OF A TURN'S PRE-TURN RETRIEVAL. `note` is the only outcome that reaches the model; the rest are
|
|
2041
|
-
* the ordinary ways it declines, and none of them is an error — each is a turn that proceeded exactly as it
|
|
2042
|
-
* would have without the feature.
|
|
2043
|
-
*
|
|
2044
|
-
* ineligible — the prompt failed a lexical gate before retrieval ran (it named its own file, or said nothing
|
|
2045
|
-
* to search for). By design, and the largest share by far.
|
|
2046
|
-
* deadline — the resident engine outran its budget and the turn went on without it.
|
|
2047
|
-
* indexing — the index had not caught up with disk, so any answer would have been confidently partial.
|
|
2048
|
-
* no-hits — the query matched nothing.
|
|
2049
|
-
* failed — retrieval threw; swallowed on purpose, since this is an optimisation nobody asked for.
|
|
2050
|
-
*
|
|
2051
|
-
* It lives in the contract rather than in the daemon because two things downstream of the daemon have to speak
|
|
2052
|
-
* it: the ledger row that records it (UsageTurnSchema.iqContextOutcome) and the experiment that reads it back
|
|
2053
|
-
* (TurnExperimentSchema.outcomes). Declared here, above both. */
|
|
2054
|
-
export const IqContextOutcomeSchema = z.enum(["note", "ineligible", "deadline", "indexing", "no-hits", "failed"]);
|
|
2055
|
-
export type IqContextOutcome = z.infer<typeof IqContextOutcomeSchema>;
|
|
2056
|
-
|
|
2057
2040
|
/* ONE METRIC'S READING of a turn-level experiment: the two arms, and whatever the arithmetic over them will
|
|
2058
2041
|
* stand behind. An experiment can carry several — see TurnExperimentSchema.
|
|
2059
2042
|
*
|
|
2060
2043
|
* `metric` says what `mean` counts and what `deltaPct` is a delta in, and choosing it is most of the work.
|
|
2061
2044
|
* proseChars — the terse steer: the thing it steers, and the only part of the model's output that
|
|
2062
2045
|
* responds to being asked to be brief (UsageTurn.proseChars has why output tokens cannot).
|
|
2063
|
-
* searchCalls —
|
|
2064
|
-
* openingSearches —
|
|
2065
|
-
*
|
|
2066
|
-
*
|
|
2067
|
-
*
|
|
2046
|
+
* searchCalls — the search teaching: the searches a turn ran, which the teaching directly changes.
|
|
2047
|
+
* openingSearches — the same, narrower: the searches before the turn first touched a file.
|
|
2048
|
+
* Search mechanisms must not be judged on COST. Cost is a whole turn's work, a search mechanism moves one part
|
|
2049
|
+
* of it, and the part sits inside the noise of the rest exactly as the steer's effect once sat inside its
|
|
2050
|
+
* tool-call arguments. */
|
|
2068
2051
|
export const TurnMetricReadingSchema = z.object({
|
|
2069
2052
|
metric: z.enum(["proseChars", "searchCalls", "openingSearches"]),
|
|
2070
2053
|
on: SavingsArmSchema,
|
|
@@ -2112,11 +2095,11 @@ export type TurnMetricReading = z.infer<typeof TurnMetricReadingSchema>;
|
|
|
2112
2095
|
* counted: a turn under a custom system prompt drops the terse steer along with everything else the daemon
|
|
2113
2096
|
* appends, so it belongs to neither arm.
|
|
2114
2097
|
*
|
|
2115
|
-
* ONE COIN FLIP, SEVERAL READINGS. `metrics` is a list because
|
|
2116
|
-
* turn ran, and the ones it ran before touching a file — and they are two readings of the SAME
|
|
2117
|
-
* two experiments. Splitting them into separate entries would duplicate the arm assignment and
|
|
2118
|
-
*
|
|
2119
|
-
*
|
|
2098
|
+
* ONE COIN FLIP, SEVERAL READINGS. `metrics` is a list because the search teaching is judged on two — the
|
|
2099
|
+
* searches a turn ran, and the ones it ran before touching a file — and they are two readings of the SAME
|
|
2100
|
+
* experiment, not two experiments. Splitting them into separate entries would duplicate the arm assignment and
|
|
2101
|
+
* let a screen show a turn count on one that disagrees with the other. Headline first: the screens read
|
|
2102
|
+
* `metrics[0]` for the big number and the rest as supporting lines. */
|
|
2120
2103
|
export const TurnExperimentSchema = z.object({
|
|
2121
2104
|
// A head and a tail rather than a plain array, because an experiment judged on nothing is not an experiment:
|
|
2122
2105
|
// the screens take the first reading for their headline and stack the rest under it, and this is what makes
|
|
@@ -2133,32 +2116,16 @@ export const TurnExperimentSchema = z.object({
|
|
|
2133
2116
|
// Content-addressed treatment version. Present where mixing rows from two instruction revisions would turn
|
|
2134
2117
|
// one experiment into two unnamed ones; the reader filters to this (latest) cohort.
|
|
2135
2118
|
cohort: z.string().optional(),
|
|
2136
|
-
/* How much of the treatment arm the treatment actually REACHED, when that is knowable and less than all of
|
|
2137
|
-
* it — pre-injection's arm is the coin flip (intention-to-treat, deliberately), and a turn can be assigned
|
|
2138
|
-
* the retrieval and still have nothing to prepend. Measured at four turns in five, which is the difference
|
|
2139
|
-
* between a mechanism worth little and one worth five times what the delta says.
|
|
2140
|
-
*
|
|
2141
|
-
* Absent ⇒ delivery is not a separate question for this experiment (the terse steer always lands) or no
|
|
2142
|
-
* turn in the window recorded it. The screen shows the delta as diluted rather than silently scaling it:
|
|
2143
|
-
* the correction is a division by a rate this small only when the rate is itself well measured. */
|
|
2144
|
-
deliveredPct: z.number().optional(),
|
|
2145
|
-
/* WHERE THE REST OF THE TREATMENT ARM WENT, most common first. `deliveredPct` says a mechanism reached one
|
|
2146
|
-
* turn in five; this says whether the other four were the eligibility gate declining on purpose or a
|
|
2147
|
-
* two-second deadline quietly eating the feature, which are the same number and opposite problems.
|
|
2148
|
-
*
|
|
2149
|
-
* Absent ⇒ the experiment has no delivery question (the terse steer always lands) or no turn recorded one. */
|
|
2150
|
-
outcomes: z.array(z.object({ outcome: IqContextOutcomeSchema, turns: z.number() })).optional(),
|
|
2151
2119
|
});
|
|
2152
2120
|
export type TurnExperiment = z.infer<typeof TurnExperimentSchema>;
|
|
2153
2121
|
|
|
2154
|
-
// `output`/`
|
|
2122
|
+
// `output`/`search` are absent when that experiment isn't running at all (its flag off, or no holdout set) — a
|
|
2155
2123
|
// section that isn't there reads as "not measured", which is the truth, while zeros would read as "measured,
|
|
2156
2124
|
// worth nothing".
|
|
2157
2125
|
export const SavingsReportSchema = z.object({
|
|
2158
2126
|
input: InputSavingsSchema,
|
|
2159
2127
|
output: TurnExperimentSchema.optional(),
|
|
2160
2128
|
search: TurnExperimentSchema.optional(),
|
|
2161
|
-
context: TurnExperimentSchema.optional(),
|
|
2162
2129
|
});
|
|
2163
2130
|
export type SavingsReport = z.infer<typeof SavingsReportSchema>;
|
|
2164
2131
|
|
|
@@ -2507,6 +2474,52 @@ export const GitLogQuerySchema = RepoParamSchema.extend({
|
|
|
2507
2474
|
// Every real git repo under /work as root-relative dir ids ("root" is implicit — the /work repo itself).
|
|
2508
2475
|
export const GitReposSchema = z.object({ repos: z.array(z.string()) });
|
|
2509
2476
|
export type GitRepos = z.infer<typeof GitReposSchema>;
|
|
2477
|
+
|
|
2478
|
+
/* WHERE EACH WORKSPACE REPO LIVES ONLINE — one entry per repo that has a parseable remote, as the host and the
|
|
2479
|
+
* `owner/name` project it names. Separate from `repos` above rather than folded into it because that route is
|
|
2480
|
+
* on the file tree's hot path and this costs a `git remote -v` per repo; a caller that wants to recognise a
|
|
2481
|
+
* workspace repo in somebody else's list (the publisher claim does exactly that) asks for it deliberately.
|
|
2482
|
+
*
|
|
2483
|
+
* A repo with no remote, or one naming a local path, is absent rather than present-and-empty: "this repo is
|
|
2484
|
+
* nowhere online" and "this repo is at X" are different answers and only one of them can be matched against. */
|
|
2485
|
+
export const GitRemoteRepoSchema = z.object({ repo: z.string(), host: z.string(), project: z.string() });
|
|
2486
|
+
export type GitRemoteRepo = z.infer<typeof GitRemoteRepoSchema>;
|
|
2487
|
+
export const GitRemoteReposSchema = z.object({ repos: z.array(GitRemoteRepoSchema) });
|
|
2488
|
+
export type GitRemoteRepos = z.infer<typeof GitRemoteReposSchema>;
|
|
2489
|
+
|
|
2490
|
+
/* PUT ONE FILE ON THE DEFAULT BRANCH AND PUBLISH IT — write, commit that path alone, push, in one call.
|
|
2491
|
+
*
|
|
2492
|
+
* One route rather than three because the interesting states are the ones BETWEEN the steps: a file written but
|
|
2493
|
+
* not committed, or committed but not pushed, is a repo the user now has to clean up by hand, and a browser
|
|
2494
|
+
* making three requests owns that mess without being able to describe it. Here the caller gets one answer that
|
|
2495
|
+
* says how far it got.
|
|
2496
|
+
*
|
|
2497
|
+
* `message` is the caller's because the commit shows up in the user's own history and a generic subject there
|
|
2498
|
+
* is litter. */
|
|
2499
|
+
export const GitPublishFileSchema = RepoParamSchema.extend({ path: z.string().min(1), content: z.string(), message: z.string().min(1) });
|
|
2500
|
+
|
|
2501
|
+
/* HOW FAR THE PUBLISH GOT, in the terms the screen has to explain it in. `ok` is "the file is on the default
|
|
2502
|
+
* branch of the remote" and nothing less — the only state that makes a public read of it succeed.
|
|
2503
|
+
*
|
|
2504
|
+
* The three steps are reported SEPARATELY because every boundary between them is a state a user can be left
|
|
2505
|
+
* in and would otherwise have to discover: a file written but not committed, a commit that exists locally but
|
|
2506
|
+
* was refused by the remote for credentials. Each of those needs a different sentence and a different next
|
|
2507
|
+
* move, and one `ok: false` cannot carry either. It is also what tells the daemon whether the worktree moved
|
|
2508
|
+
* at all, which decides whether this counts as a user write on the timeline.
|
|
2509
|
+
*
|
|
2510
|
+
* `branch` and `defaultBranch` ride along so a refusal can name both sides of the mismatch rather than saying
|
|
2511
|
+
* "wrong branch" at someone who cannot see which one they are on. */
|
|
2512
|
+
export const GitPublishFileResultSchema = z.object({
|
|
2513
|
+
ok: z.boolean(),
|
|
2514
|
+
wrote: z.boolean(),
|
|
2515
|
+
committed: z.boolean(),
|
|
2516
|
+
pushed: z.boolean(),
|
|
2517
|
+
branch: z.string().optional(),
|
|
2518
|
+
defaultBranch: z.string().optional(),
|
|
2519
|
+
reason: z.string().optional(),
|
|
2520
|
+
});
|
|
2521
|
+
export type GitPublishFileResult = z.infer<typeof GitPublishFileResultSchema>;
|
|
2522
|
+
|
|
2510
2523
|
export const GitCommitDiffQuerySchema = RepoParamSchema.extend({ sha: ShaSchema });
|
|
2511
2524
|
// A commit's changed files (vs its first parent; a root commit vs the empty tree) — the graph's detail tree
|
|
2512
2525
|
// renders these (line stats included) and reuses the diff UI on click. Just GitChanges: the line stats live on
|
|
@@ -3871,12 +3884,25 @@ export const CapabilityStatusSchema = z.object({
|
|
|
3871
3884
|
code: z.string().optional(),
|
|
3872
3885
|
});
|
|
3873
3886
|
export type CapabilityStatus = z.infer<typeof CapabilityStatusSchema>;
|
|
3874
|
-
|
|
3887
|
+
/* The list row: manifest entry + live status. Secrets are never returned (an mcp token becomes hasToken).
|
|
3888
|
+
*
|
|
3889
|
+
* `secrets` NAMES them without carrying them — the config keys this connection is actually holding a credential
|
|
3890
|
+
* under. It is what makes an edit form possible at all: `config` is everything the browser may see, so a form
|
|
3891
|
+
* seeded from it alone cannot tell "this tunnel has a pre-shared key I'm not allowed to show you" from "this
|
|
3892
|
+
* tunnel has no pre-shared key", and both render as an empty required box. Saving one then wipes the
|
|
3893
|
+
* credential, which is why changing a routed network used to mean re-typing a key.
|
|
3894
|
+
*
|
|
3895
|
+
* Keys, never values, and never a boolean per known field: the set is derived from what the entry stores, so a
|
|
3896
|
+
* field the user left blank is absent and a card that gained a credential since is present. The form reads it as
|
|
3897
|
+
* "show dots, and let blank mean keep" (VAULTED — capability-secrets.ts). */
|
|
3875
3898
|
export const CapabilitySummarySchema = z.object({
|
|
3876
3899
|
id: z.string(),
|
|
3877
3900
|
kind: CapabilityKindSchema,
|
|
3878
3901
|
status: CapabilityStatusSchema,
|
|
3879
3902
|
config: z.record(z.string(), z.union([z.string(), z.number(), z.boolean()])),
|
|
3903
|
+
// Defaulted for the daemon-older-than-browser seam, like `recommendations` below: a required field would
|
|
3904
|
+
// fail the whole list parse against a sandbox predating this, taking the page down to hide some dots.
|
|
3905
|
+
secrets: z.array(z.string()).default([]),
|
|
3880
3906
|
});
|
|
3881
3907
|
/* A capability the WORKSPACE asks for but the manifest doesn't carry — derived from what is checked out under
|
|
3882
3908
|
* /work, not from anything the user configured. It exists because the failures it prevents are illegible: a
|
|
@@ -5421,19 +5447,23 @@ export type MachineSandbox = z.infer<typeof MachineSandboxSchema>;
|
|
|
5421
5447
|
/* ONE OPERATION ON ONE SANDBOX ON ONE MACHINE — the Computers view's buttons, and the only thing that changes a
|
|
5422
5448
|
* machine's fleet from a browser.
|
|
5423
5449
|
*
|
|
5424
|
-
* All
|
|
5425
|
-
* behave underneath: three are a docker call that returns in a second,
|
|
5450
|
+
* All nine ops travel one route because they are one decision to the person clicking, however differently they
|
|
5451
|
+
* behave underneath: three are a docker call that returns in a second, four run the `ic` flow for minutes, one
|
|
5426
5452
|
* deletes, and one only reads. Splitting them by duration would put the same button on two doors and give the
|
|
5427
5453
|
* view two shapes to render. So every op answers as a STREAM of lines ending in a result — the fast ones simply
|
|
5428
5454
|
* have little to say, and `logs` is the case where the lines ARE the answer.
|
|
5429
5455
|
*
|
|
5430
|
-
* `
|
|
5431
|
-
*
|
|
5432
|
-
*
|
|
5456
|
+
* `prepare` is the one that changes nothing on purpose: it downloads and builds the next update and stops
|
|
5457
|
+
* there, leaving the container running the image it was already running. It is what turns `update` from a wait
|
|
5458
|
+
* of minutes into a restart of seconds, and it is safe to offer at any moment for exactly that reason.
|
|
5433
5459
|
*
|
|
5434
|
-
*
|
|
5435
|
-
*
|
|
5436
|
-
|
|
5460
|
+
* `logs` is here rather than on a route of its own for the same reason the rest share it: it is a button in the
|
|
5461
|
+
* same row as the others, on a container that may be too broken to answer any other way, and the stream shape
|
|
5462
|
+
* already carries "many lines, then an outcome" exactly as a log tail wants to arrive.
|
|
5463
|
+
*
|
|
5464
|
+
* The machine enforces which of them it will do: `sandboxes` covers everything but removal, which takes its own
|
|
5465
|
+
* switch, and a refusal comes back as the machine's own sentence naming the control to flip. */
|
|
5466
|
+
export const MachineSandboxOpSchema = z.enum(["start", "stop", "restart", "prepare", "update", "rebuild", "rollback", "remove", "logs"]);
|
|
5437
5467
|
export type MachineSandboxOp = z.infer<typeof MachineSandboxOpSchema>;
|
|
5438
5468
|
|
|
5439
5469
|
export const MachineSandboxFlowSchema = z.object({
|
|
@@ -5512,8 +5542,25 @@ export type MachinePort = z.infer<typeof MachinePortSchema>;
|
|
|
5512
5542
|
export const MachineWatcherSchema = z.object({
|
|
5513
5543
|
running: z.boolean(),
|
|
5514
5544
|
pid: z.number().int().optional(),
|
|
5545
|
+
/* When the watcher last FINISHED a pass — the field that makes `running` mean something. The agent holds its
|
|
5546
|
+
* SSH transport listeners on its own event loop, so a failure that escapes the loop leaves a process that is
|
|
5547
|
+
* alive and a loop that is gone: pid present, unit "active", mirroring and the git bridge stopped. Absent
|
|
5548
|
+
* means the agent has not reported one (too old to stamp, or its first pass has not landed) — which is not
|
|
5549
|
+
* the same as stalled, and readers must not treat it as either state. */
|
|
5515
5550
|
lastTickAt: z.number().optional(),
|
|
5516
5551
|
});
|
|
5552
|
+
export type MachineWatcher = z.infer<typeof MachineWatcherSchema>;
|
|
5553
|
+
|
|
5554
|
+
/* How long a watcher may go without finishing a pass before "running" stops being the honest word for it. Its
|
|
5555
|
+
* loop polls every 5s and its slowest step is bounded by two 10s network timeouts per pairing, so a minute is
|
|
5556
|
+
* several passes of slack — the same yardstick the Computers view already ages a whole report by.
|
|
5557
|
+
*
|
|
5558
|
+
* The rule lives HERE, next to the field, because the terminal and the browser both answer this question and a
|
|
5559
|
+
* machine that is "running" in one and "stalled" in the other is worse than either answer alone. */
|
|
5560
|
+
export const WATCHER_STALL_AFTER_MS = 60_000;
|
|
5561
|
+
|
|
5562
|
+
export const watcherStalled = (watcher: MachineWatcher, now: number): boolean =>
|
|
5563
|
+
watcher.running && watcher.lastTickAt !== undefined && now - watcher.lastTickAt > WATCHER_STALL_AFTER_MS;
|
|
5517
5564
|
|
|
5518
5565
|
export const MachineReportSchema = z.object({
|
|
5519
5566
|
/* The OS hostname, and the JOIN KEY. A machine can arrive here two ways at once — volunteered by its sync
|
|
@@ -6198,6 +6245,31 @@ export const AdapterHealthSchema = z.object({
|
|
|
6198
6245
|
});
|
|
6199
6246
|
export type AdapterHealthReport = z.infer<typeof AdapterHealthSchema>;
|
|
6200
6247
|
|
|
6248
|
+
/* AN UPDATE ALREADY DOWNLOADED AND BUILT, waiting for the restart that applies it.
|
|
6249
|
+
*
|
|
6250
|
+
* An update is one blocking operation but it was never one kind of work: pulling the new image and re-applying
|
|
6251
|
+
* the environment recipe take the overwhelming majority of the wall clock, and the sandbox is up and serving
|
|
6252
|
+
* through both of them. Only the cutover is downtime, and it is seconds.
|
|
6253
|
+
*
|
|
6254
|
+
* The daemon cannot know any of this by itself — it holds no host Docker socket — so `ic sandbox prepare`
|
|
6255
|
+
* tells it, on the machine that runs the container. That is the whole reason this exists: without it, the
|
|
6256
|
+
* update card had to quote the download as if it were an outage, and "a few minutes, this page loses the
|
|
6257
|
+
* sandbox" is a completely different decision from "about half a minute".
|
|
6258
|
+
*
|
|
6259
|
+
* Advisory only, in the strict sense: it decides what a card SAYS and never what gets installed. The swap
|
|
6260
|
+
* re-derives every one of these facts from the host-side record and refuses the fast path if any has drifted. */
|
|
6261
|
+
export const StagedUpdateSchema = z.object({
|
|
6262
|
+
// The version the staged image reports about itself. Absent when the image would not say (an older build,
|
|
6263
|
+
// a probe that failed), which reads as "ready, version unknown" — never as nothing being ready.
|
|
6264
|
+
version: z.string().optional(),
|
|
6265
|
+
// The release channel it was staged FROM, which is not necessarily the one this sandbox follows: preparing
|
|
6266
|
+
// a beta build is not moving onto beta.
|
|
6267
|
+
channel: z.string(),
|
|
6268
|
+
// When it finished downloading, epoch ms — what answers "is this still the update I am being offered?"
|
|
6269
|
+
at: z.number(),
|
|
6270
|
+
});
|
|
6271
|
+
export type StagedUpdate = z.infer<typeof StagedUpdateSchema>;
|
|
6272
|
+
|
|
6201
6273
|
export const InfoSchema = z.object({
|
|
6202
6274
|
name: z.string().optional(),
|
|
6203
6275
|
image: z.string().optional(),
|
|
@@ -6233,6 +6305,9 @@ export const InfoSchema = z.object({
|
|
|
6233
6305
|
* turns the update card from an offer into a warning that asks to be read before it hands over the
|
|
6234
6306
|
* command. Absent for the overwhelming majority of updates, which break nothing. */
|
|
6235
6307
|
breakingNotes: z.array(z.string()).optional(),
|
|
6308
|
+
/* AN UPDATE THAT HAS ALREADY BEEN DOWNLOADED AND BUILT on the machine that runs this container, and is
|
|
6309
|
+
* waiting for the restart that applies it. Absent for the ordinary case where nothing is staged. */
|
|
6310
|
+
staged: StagedUpdateSchema.optional(),
|
|
6236
6311
|
});
|
|
6237
6312
|
export type Info = z.infer<typeof InfoSchema>;
|
|
6238
6313
|
|
|
@@ -6394,31 +6469,6 @@ export const UsageTurnSchema = z.object({
|
|
|
6394
6469
|
* no control to be compared against. Pooling those into the off-arm would compare steered turns against a
|
|
6395
6470
|
* population selected by something other than the coin flip, which is not a control at all. */
|
|
6396
6471
|
terse: z.boolean().optional(),
|
|
6397
|
-
/* Which arm of the pre-injection experiment this turn ran on (settings.iqContextHoldout), on the same terms
|
|
6398
|
-
* as `terse` above: absent ⇒ outside the experiment.
|
|
6399
|
-
*
|
|
6400
|
-
* TRUE means the turn was ASSIGNED the retrieved context, not that a note was necessarily prepended — a
|
|
6401
|
-
* treatment turn whose retrieval came back empty or unconfident injects nothing. That is deliberate: the
|
|
6402
|
-
* arms have to be the coin flip's populations, and re-labelling a turn by what retrieval happened to find
|
|
6403
|
-
* would sort turns by how searchable their question was, which is a property of the question. The control
|
|
6404
|
-
* arm contains the same unsearchable questions in the same proportion, so they cancel. */
|
|
6405
|
-
iqContext: z.boolean().optional(),
|
|
6406
|
-
/* WHAT ACTUALLY HAPPENED to the retrieval on this turn — the companion to `iqContext`, and the answer to the
|
|
6407
|
-
* question that field's design deliberately refuses to answer.
|
|
6408
|
-
*
|
|
6409
|
-
* Keeping the arm on the coin flip is right, and it costs something: the treatment arm contains turns the
|
|
6410
|
-
* treatment never reached, so the delta it yields is diluted by however many those are. Measured over nine
|
|
6411
|
-
* days of real use that was four turns in five, which makes the difference between "this mechanism is worth
|
|
6412
|
-
* little" and "this mechanism is worth five times what the number says".
|
|
6413
|
-
*
|
|
6414
|
-
* IT RECORDS THE REASON, not merely a yes/no, because the yes/no left the whole loss unexplained. 81% of an
|
|
6415
|
-
* assigned arm delivering nothing is either a gate doing its job or a deadline quietly eating the feature,
|
|
6416
|
-
* and those call for opposite responses — one is working as designed, the other is a two-second timeout to
|
|
6417
|
-
* raise. A boolean cannot tell them apart, so nothing downstream could act on the number it produced.
|
|
6418
|
-
*
|
|
6419
|
-
* `note` is delivery; every other value is a turn that ran exactly as the control arm would have, labelled
|
|
6420
|
-
* with what took the treatment away. Absent ⇒ outside the experiment, exactly as for the arm. */
|
|
6421
|
-
iqContextOutcome: IqContextOutcomeSchema.optional(),
|
|
6422
6472
|
/* Which arm of the iq SEARCH-TEACHING experiment this conversation runs on
|
|
6423
6473
|
* (settings.iqSearchHoldout). Stable for every turn in one conversation: the treatment is instruction
|
|
6424
6474
|
* loaded into a provider session, so flipping it per turn would call a remembered treatment a control.
|
|
@@ -6427,10 +6477,6 @@ export const UsageTurnSchema = z.object({
|
|
|
6427
6477
|
// Hash of the plugin nudge + skill body used for this arm. Control turns carry it too, so a report can keep
|
|
6428
6478
|
// both sides of one treatment revision together and exclude older wording after an upgrade.
|
|
6429
6479
|
iqSearchCohort: z.string().optional(),
|
|
6430
|
-
/* Wall time spent deciding the pre-turn retrieval outcome, including the full deadline on a timeout.
|
|
6431
|
-
* Absent on rows written before latency was recorded or turns outside the retrieval experiment. Keeping it
|
|
6432
|
-
* per outcome is what lets the report distinguish a slow engine from an eligibility gate. */
|
|
6433
|
-
iqContextDurationMs: z.number().nonnegative().optional(),
|
|
6434
6480
|
/* Characters of the model's own PROSE this turn — the `delta` frames only, so no tool-call arguments and no
|
|
6435
6481
|
* thinking. What the terse steer is judged on, and the reason it can be judged at all.
|
|
6436
6482
|
*
|
|
@@ -6449,33 +6495,29 @@ export const UsageTurnSchema = z.object({
|
|
|
6449
6495
|
proseChars: z.number().optional(),
|
|
6450
6496
|
/* SEARCHES THIS TURN RAN — every tool call that went looking for code, the dedicated search tools and the
|
|
6451
6497
|
* CLI searches alike (isSearchCall owns the rule; `iq q` is Bash and would otherwise not be counted at all).
|
|
6452
|
-
* What
|
|
6498
|
+
* What the search teaching is judged on, and the same correction `proseChars` is to the terse steer.
|
|
6453
6499
|
*
|
|
6454
|
-
* COST PER TURN CANNOT SERVE
|
|
6455
|
-
*
|
|
6456
|
-
*
|
|
6457
|
-
*
|
|
6458
|
-
* handing the treatment arm the bigger jobs. Cost is a whole turn's worth of work, and retrieval touches one
|
|
6459
|
-
* part of it, so the part lives inside the noise of the rest — exactly the shape that made output tokens
|
|
6460
|
-
* unable to see the steer.
|
|
6500
|
+
* COST PER TURN CANNOT SERVE: cost is a whole turn's worth of work, a search mechanism touches one part of
|
|
6501
|
+
* it, and the part lives inside the noise of the rest — exactly the shape that made output tokens unable to
|
|
6502
|
+
* see the steer. Nine days of a since-removed retrieval experiment proved it with an interval from −2.9% to
|
|
6503
|
+
* +56.9%, driven entirely by which arm had drawn the bigger jobs.
|
|
6461
6504
|
*
|
|
6462
|
-
* Searches are what the mechanism acts on directly
|
|
6463
|
-
*
|
|
6464
|
-
*
|
|
6505
|
+
* Searches are what the mechanism acts on directly. Turns that never search stay in the population at zero
|
|
6506
|
+
* rather than being filtered out — they dilute both arms equally, while selecting on "did it search" would
|
|
6507
|
+
* select on the treatment itself.
|
|
6465
6508
|
*
|
|
6466
6509
|
* Absent ⇒ the turn predates this being measured; `armOf` drops it rather than reading it as a turn that
|
|
6467
6510
|
* searched nothing. */
|
|
6468
6511
|
searchCalls: z.number().optional(),
|
|
6469
|
-
/* …and how many of them came BEFORE the turn first opened or changed a file — the orientation burst
|
|
6470
|
-
*
|
|
6471
|
-
* that doesn't goes hunting first, and pre-injection's whole claim is that it removes that hunt.
|
|
6512
|
+
/* …and how many of them came BEFORE the turn first opened or changed a file — the orientation burst. A turn
|
|
6513
|
+
* that already knows where to look starts working; one that doesn't goes hunting first.
|
|
6472
6514
|
*
|
|
6473
6515
|
* The narrower of the two readings and the less confounded: `searchCalls` still grows with the size of the
|
|
6474
6516
|
* job, while the walk up to the first file is roughly the same act whatever the job turns out to be.
|
|
6475
6517
|
*
|
|
6476
6518
|
* A turn that never reads or edits counts all of its searches here — it never arrived, so all of it was
|
|
6477
|
-
* orientation. Dropping those instead would select the population by an OUTCOME the treatment moves
|
|
6478
|
-
*
|
|
6519
|
+
* orientation. Dropping those instead would select the population by an OUTCOME the treatment moves, which
|
|
6520
|
+
* is the one bias an arm-based reading cannot absorb.
|
|
6479
6521
|
*
|
|
6480
6522
|
* Absent ⇒ as for `searchCalls`. */
|
|
6481
6523
|
openingSearches: z.number().optional(),
|
|
@@ -4,6 +4,7 @@ import { describe, expect, it } from "vitest";
|
|
|
4
4
|
import {
|
|
5
5
|
isLockedWorkspacePath,
|
|
6
6
|
isReportedManifest,
|
|
7
|
+
isReviewableLockedPath,
|
|
7
8
|
REPORTED_MANIFEST_PATHS,
|
|
8
9
|
staleQueryKeys,
|
|
9
10
|
VERSIONED_STATE_PATHS,
|
|
@@ -229,6 +230,45 @@ describe(`isLockedWorkspacePath`, () => {
|
|
|
229
230
|
});
|
|
230
231
|
});
|
|
231
232
|
|
|
233
|
+
/* The carve-out the diff routes ask for, and the reason it is derived: a locked entry the root repo TRACKS has
|
|
234
|
+
* a diff by construction, and refusing to serve it made the Changes panel list a row it could not open. */
|
|
235
|
+
describe(`isReviewableLockedPath`, () => {
|
|
236
|
+
it(`admits the locked entry the root repo tracks, and nothing else locked`, () => {
|
|
237
|
+
expect(isReviewableLockedPath(`.intentic/capabilities.json`)).toBe(true);
|
|
238
|
+
// Every other locked entry is a credential, an identity binding or private runtime state. None is
|
|
239
|
+
// versioned, so none is reachable through a diff — the carve-out cannot widen without the flag.
|
|
240
|
+
expect(isReviewableLockedPath(`.intentic/owner.json`)).toBe(false);
|
|
241
|
+
expect(isReviewableLockedPath(`.intentic/members.json`)).toBe(false);
|
|
242
|
+
expect(isReviewableLockedPath(`.intentic/ci.json`)).toBe(false);
|
|
243
|
+
expect(isReviewableLockedPath(`.intentic/auth/codex/auth.json`)).toBe(false);
|
|
244
|
+
expect(isReviewableLockedPath(`.intentic/sessions/claude/x.jsonl`)).toBe(false);
|
|
245
|
+
expect(isReviewableLockedPath(`.intentic/browser/Default/Cookies`)).toBe(false);
|
|
246
|
+
expect(isReviewableLockedPath(`.git/config`)).toBe(false);
|
|
247
|
+
});
|
|
248
|
+
|
|
249
|
+
it(`answers only for the locked set — an ordinary path was never refused to begin with`, () => {
|
|
250
|
+
// Tracked, but not locked: the guards never ask this of them, and a `true` here would read as "this
|
|
251
|
+
// path needed a carve-out", which is a different and wrong statement.
|
|
252
|
+
expect(isReviewableLockedPath(`.intentic/settings.json`)).toBe(false);
|
|
253
|
+
expect(isReviewableLockedPath(`src/app.ts`)).toBe(false);
|
|
254
|
+
// A repo's own nested state dir is its project's content, exactly as the lock reads it.
|
|
255
|
+
expect(isReviewableLockedPath(`myrepo/.intentic/capabilities.json`)).toBe(false);
|
|
256
|
+
});
|
|
257
|
+
|
|
258
|
+
it(`reads a platform path and a dot-relative one the same as a posix one`, () => {
|
|
259
|
+
expect(isReviewableLockedPath(`.intentic\\capabilities.json`)).toBe(true);
|
|
260
|
+
expect(isReviewableLockedPath(`./.intentic/capabilities.json`)).toBe(true);
|
|
261
|
+
});
|
|
262
|
+
|
|
263
|
+
it(`stays a strict subset of the lock`, () => {
|
|
264
|
+
// The carve-out is about which locked paths a DIFF may serve. A path it admits that the lock never
|
|
265
|
+
// held would mean the guards had stopped agreeing on what the control plane is.
|
|
266
|
+
for (const path of VERSIONED_STATE_PATHS) {
|
|
267
|
+
expect([path, isReviewableLockedPath(path) && !isLockedWorkspacePath(path)]).toEqual([path, false]);
|
|
268
|
+
}
|
|
269
|
+
});
|
|
270
|
+
});
|
|
271
|
+
|
|
232
272
|
describe(`VERSIONED_STATE_PATHS`, () => {
|
|
233
273
|
/* THE ONE ASSERTION THAT MUST NEVER GO GREEN BY ACCIDENT.
|
|
234
274
|
*
|
package/src/workspace-state.ts
CHANGED
|
@@ -133,7 +133,12 @@ const STATE_FILES = [
|
|
|
133
133
|
* left a diff nowhere. One consequence worth stating rather than discovering: an identifier that pairs with a
|
|
134
134
|
* credential — Komodo's api key beside its api secret, which its own connector card calls "like a database
|
|
135
135
|
* user" — is echoed, and therefore lands in the diff exactly as a database username would. */
|
|
136
|
-
{
|
|
136
|
+
{
|
|
137
|
+
path: ".intentic/capabilities.json",
|
|
138
|
+
invalidates: ["capabilities", "environment", "panels", "manifests"],
|
|
139
|
+
portability: "carry",
|
|
140
|
+
versioned: true,
|
|
141
|
+
},
|
|
137
142
|
|
|
138
143
|
/* Which workspace-derived recommendations the owner has said "not needed" to, and the evidence each was
|
|
139
144
|
* declined against. It rides the `capabilities` key because the catalog is what changes when one lands, and
|
|
@@ -692,6 +697,29 @@ export const isLockedWorkspacePath = (relPath: string): boolean => {
|
|
|
692
697
|
return segments.length >= 2 && segments[0] === STATE_DIR && LOCKED_STATE_ENTRIES.has(segments[1] ?? "");
|
|
693
698
|
};
|
|
694
699
|
|
|
700
|
+
/* THE LOCKED ENTRIES THE ROOT REPO TRACKS — refused by the file API, and diffable anyway.
|
|
701
|
+
*
|
|
702
|
+
* `capabilities.json` is the only one today and the whole reason this exists. Both of its rules are right on
|
|
703
|
+
* their own: it is `versioned`, because connecting this sandbox to a deployment orchestrator is the largest
|
|
704
|
+
* change anyone makes to what it can DO and that belongs in review; and it is locked, because a member who
|
|
705
|
+
* could PUT one through the generic file API would be granting themselves a capability the owner never
|
|
706
|
+
* approved. The lock was always about that WRITE — its credentials live in the vault, not in the file.
|
|
707
|
+
*
|
|
708
|
+
* Together, though, the second silently cancelled the first. The Changes panel listed the file (git tracks it,
|
|
709
|
+
* so `git status` reports it), and clicking the row asked a diff route that refuses every control-plane path —
|
|
710
|
+
* a 404 on the one surface `versioned` exists to produce. The bytes were already in `git log`, in every clone
|
|
711
|
+
* of the root repo and in the workspace search; only the review was missing.
|
|
712
|
+
*
|
|
713
|
+
* So the review surfaces ask THIS instead of the flat lock, and it derives from the same flag rather than
|
|
714
|
+
* naming the file, so marking another locked entry `versioned` cannot reproduce the contradiction. Every other
|
|
715
|
+
* surface — read, write, move, delete, publish — still asks `isLockedWorkspacePath` and still refuses.
|
|
716
|
+
*
|
|
717
|
+
* Accepts either slash, like the rule above it. */
|
|
718
|
+
export const isReviewableLockedPath = (relPath: string): boolean => {
|
|
719
|
+
const rel = relPath.replaceAll("\\", "/").replace(/^\.\//, "");
|
|
720
|
+
return isLockedWorkspacePath(rel) && VERSIONED_STATE_PATHS.some((path) => (path.endsWith("/") ? rel.startsWith(path) : rel === path));
|
|
721
|
+
};
|
|
722
|
+
|
|
695
723
|
/* Every path this table declares, as a type. `as const` above is what makes it one, and it is what finally makes
|
|
696
724
|
* the first sentence of this file's header TRUE rather than aspirational.
|
|
697
725
|
*
|