@intentic/sandbox-contract 1.174.0 → 1.175.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/contracts/capabilities.contract.d.ts +12 -2
- package/dist/contracts/capabilities.contract.d.ts.map +1 -1
- package/dist/contracts/extensions.contract.d.ts +1 -0
- package/dist/contracts/extensions.contract.d.ts.map +1 -1
- package/dist/contracts/git.contract.d.ts +5 -1
- package/dist/contracts/git.contract.d.ts.map +1 -1
- package/dist/contracts/komodo.contract.d.ts +18 -0
- package/dist/contracts/komodo.contract.d.ts.map +1 -1
- package/dist/contracts/komodo.contract.js +2 -1
- package/dist/contracts/komodo.contract.js.map +1 -1
- package/dist/contracts/workspace.contract.d.ts +6 -0
- package/dist/contracts/workspace.contract.d.ts.map +1 -1
- package/dist/contracts/workspace.contract.js +2 -1
- package/dist/contracts/workspace.contract.js.map +1 -1
- package/dist/index.d.ts +42 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/schemas.d.ts +58 -18
- package/dist/schemas.d.ts.map +1 -1
- package/dist/schemas.js +28 -10
- package/dist/schemas.js.map +1 -1
- package/dist/workspace-state.js +2 -2
- package/dist/workspace-state.js.map +1 -1
- package/package.json +3 -2
- package/src/contracts/komodo.contract.ts +4 -0
- package/src/contracts/workspace.contract.ts +6 -0
- package/src/schemas.test.ts +48 -62
- package/src/schemas.ts +102 -28
- package/src/workspace-state.ts +2 -2
package/src/schemas.test.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { expect, test } from "vitest";
|
|
2
|
-
import { CapabilitiesListSchema, SandboxSettingsSchema } from "./schemas.js";
|
|
2
|
+
import { CapabilitiesListSchema, DeployOverviewResponseSchema, SandboxSettingsSchema } from "./schemas.js";
|
|
3
3
|
|
|
4
4
|
/* The settings shape spans a version seam that really moves: the browser ships with the platform, the daemon
|
|
5
5
|
* ships inside the user's sandbox image, so a web build routinely parses a payload from an OLDER daemon. These
|
|
@@ -18,69 +18,23 @@ test("a payload from a build that predates a toggle parses, with the new toggle
|
|
|
18
18
|
outputCleaners: "-cap",
|
|
19
19
|
outputHoldout: 0.1,
|
|
20
20
|
};
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
filterBackend: "native",
|
|
27
|
-
systemPromptMode: "intentic",
|
|
28
|
-
systemPrompt: "",
|
|
29
|
-
quickModel: "",
|
|
30
|
-
agentRetentionDays: 3,
|
|
31
|
-
autoLand: true,
|
|
32
|
-
resumeAfterOutage: true,
|
|
33
|
-
autoResumeOnRestart: true,
|
|
34
|
-
prepushCommand: "",
|
|
35
|
-
prepushTimeoutMs: 900_000,
|
|
36
|
-
prepushFixModel: "",
|
|
37
|
-
prepushFixEffort: "",
|
|
38
|
-
});
|
|
21
|
+
// The defaults come from the schema, not from a copy of it written here. Transcribing them made every
|
|
22
|
+
// setting the product gained land as a failure in this file — a diff that only ever said "the list moved",
|
|
23
|
+
// never "tolerance broke", and whose fix was always to paste the new default in. What this test is about
|
|
24
|
+
// is the seam: what the old build sent survives verbatim, and what it never heard of arrives at default.
|
|
25
|
+
expect(SandboxSettingsSchema.parse(older)).toEqual({ ...SandboxSettingsSchema.parse({}), ...older });
|
|
39
26
|
});
|
|
40
27
|
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
// control that would tell you whether they paid for themselves costs the turns it measures.
|
|
52
|
-
iqContext: false,
|
|
53
|
-
iqContextHoldout: 0,
|
|
54
|
-
outputCleaners: "off",
|
|
55
|
-
outputHoldout: 0,
|
|
56
|
-
filterBackend: "native",
|
|
57
|
-
// The default base is Intentic's own prompt; the text field is only read under "custom".
|
|
58
|
-
systemPromptMode: "intentic",
|
|
59
|
-
systemPrompt: "",
|
|
60
|
-
// Empty is not "no quick model" — it is Auto, resolved from the connected accounts on every read
|
|
61
|
-
// (quick-model.ts). Storing a resolved id as the default would name a provider a fresh sandbox has no
|
|
62
|
-
// credential for, and would go stale the moment one is connected.
|
|
63
|
-
quickModel: "",
|
|
64
|
-
// The one default that isn't "off": the fleet board's Finished lane has no exit of its own, and each
|
|
65
|
-
// card it holds is a worktree checkout. Opting INTO cleanup would mean shipping a leak by default.
|
|
66
|
-
agentRetentionDays: 3,
|
|
67
|
-
// On because it is the historical behaviour — defaulting off would silently hold every existing
|
|
68
|
-
// sandbox's finished work on branches nobody is watching.
|
|
69
|
-
autoLand: true,
|
|
70
|
-
// On, where a spent usage limit re-runs nothing: an outage resume spends nothing the dead turn hadn't
|
|
71
|
-
// already committed, and the turns it saves are the unattended ones nobody is watching to restart by hand.
|
|
72
|
-
resumeAfterOutage: true,
|
|
73
|
-
// On: a daemon restart is usually intentic's own doing (an image update, an approved environment
|
|
74
|
-
// change), not the user's decision, so the turn it interrupted resumes rather than staying stuck.
|
|
75
|
-
autoResumeOnRestart: true,
|
|
76
|
-
// Empty disables the pre-push check until the owner supplies this workspace's verification command.
|
|
77
|
-
prepushCommand: "",
|
|
78
|
-
prepushTimeoutMs: 900_000,
|
|
79
|
-
// Empty ⇒ the suggested fix session opens on whatever the chat composer would have started with, which
|
|
80
|
-
// is the model the user already chose to work with.
|
|
81
|
-
prepushFixModel: "",
|
|
82
|
-
prepushFixEffort: "",
|
|
83
|
-
});
|
|
28
|
+
/* The invariant a fresh sandbox depends on: NO field is required. A settings object is written for the first
|
|
29
|
+
* time only when the user changes something, so until then the daemon parses `{}` — one field without a
|
|
30
|
+
* `.default()` turns that into a throw at boot, and the version tolerance above is built on the same property.
|
|
31
|
+
*
|
|
32
|
+
* Asserted by shape rather than by value: what each default IS belongs next to the field in schemas.ts, where
|
|
33
|
+
* the reason it holds is written down. A second copy here proved nothing the schema didn't already say and
|
|
34
|
+
* failed on every field the product added. */
|
|
35
|
+
test("no field is required — a workspace that has never written settings parses", () => {
|
|
36
|
+
const defaults = SandboxSettingsSchema.parse({});
|
|
37
|
+
expect(Object.keys(defaults).sort()).toEqual(Object.keys(SandboxSettingsSchema.shape).sort());
|
|
84
38
|
});
|
|
85
39
|
|
|
86
40
|
test("a key of the wrong type is still a parse failure — tolerance is for absence, not for garbage", () => {
|
|
@@ -98,3 +52,35 @@ test("a capability list from a daemon that predates recommendations parses, with
|
|
|
98
52
|
const older = { capabilities: [{ id: "github", kind: "cli", status: { state: "active" }, config: { provider: "github" } }] };
|
|
99
53
|
expect(CapabilitiesListSchema.parse(older).recommendations).toEqual([]);
|
|
100
54
|
});
|
|
55
|
+
|
|
56
|
+
/* The deployments board crosses the same seam and learned it the hard way. `repos` (workspace repo → Komodo
|
|
57
|
+
* stack links) shipped REQUIRED, and the first sandbox whose daemon predated it rendered
|
|
58
|
+
* `Invalid input: expected array, received undefined at repos` instead of the board — a dead page, on the one
|
|
59
|
+
* surface whose job is to say whether production is up, to hide a band of suggestions.
|
|
60
|
+
*
|
|
61
|
+
* `viewer` is the deliberate contrast: also added later, also absent from an older daemon, but OPTIONAL rather
|
|
62
|
+
* than defaulted, because its absence is information. The empty state tells "the key can see nothing" apart
|
|
63
|
+
* from "we could not tell", and defaulting it would have collapsed the two. */
|
|
64
|
+
|
|
65
|
+
test("an overview from a daemon that predates repo links parses, with no links rather than no board", () => {
|
|
66
|
+
const older = { komodoUrl: "https://komodo.example.com", reachable: true, resources: [], servers: [], alerts: [] };
|
|
67
|
+
const parsed = DeployOverviewResponseSchema.parse(older);
|
|
68
|
+
expect(parsed.repos).toEqual([]);
|
|
69
|
+
// Absent, NOT defaulted: the empty state reads this to avoid claiming an empty Komodo it cannot vouch for.
|
|
70
|
+
expect(parsed.viewer).toBeUndefined();
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
test("a board that did carry links keeps them, and garbage in them is still a failure", () => {
|
|
74
|
+
const current = {
|
|
75
|
+
komodoUrl: "https://komodo.example.com",
|
|
76
|
+
reachable: true,
|
|
77
|
+
viewer: { username: "intentic", admin: false },
|
|
78
|
+
repos: [{ repo: "app", projectName: "app", composePath: "app/compose.yaml", suggestions: ["app-prod"] }],
|
|
79
|
+
resources: [],
|
|
80
|
+
servers: [],
|
|
81
|
+
alerts: [],
|
|
82
|
+
};
|
|
83
|
+
expect(DeployOverviewResponseSchema.parse(current).repos[0]?.suggestions).toEqual(["app-prod"]);
|
|
84
|
+
// Tolerance is for absence, not for the wrong shape — a `repos` that is present and wrong is real drift.
|
|
85
|
+
expect(DeployOverviewResponseSchema.safeParse({ ...current, repos: "none" }).success).toBe(false);
|
|
86
|
+
});
|
package/src/schemas.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { ExtensionManifestSchema } from "@intentic/extension-api";
|
|
2
|
+
import { RegistryEntrySchema } from "@intentic/registry";
|
|
2
3
|
import { z } from "zod";
|
|
3
4
|
|
|
4
5
|
// All request/response wire schemas for the sandbox daemon. Inputs that carry a `{param}` in their route path
|
|
@@ -962,17 +963,21 @@ export const IntenticRunSchema = z.object({ args: z.array(z.string()) });
|
|
|
962
963
|
|
|
963
964
|
// ---- git ----
|
|
964
965
|
|
|
965
|
-
// What a commit records —
|
|
966
|
-
//
|
|
966
|
+
// What a commit records — three shapes, each a real git spelling. The last two are for the case where nothing
|
|
967
|
+
// is staged yet and the caller has said what to stage; they are alternatives, and a caller sends at most one:
|
|
967
968
|
// absent ⇒ commit whatever is staged (plain `git commit`)
|
|
969
|
+
// all: true ⇒ stage every change in the repo, then commit (`commit -a`; VSCode's "stage all and commit")
|
|
970
|
+
// paths ⇒ `git add` those repo-relative paths, then commit the index
|
|
968
971
|
//
|
|
969
|
-
//
|
|
970
|
-
// second path-selection channel alongside it
|
|
971
|
-
// staged file records the WORKTREE content while the row the user picked showed the INDEX content.
|
|
972
|
-
// the
|
|
972
|
+
// `paths` is emphatically NOT `commit --only`. The index IS git's mechanism for choosing what a commit
|
|
973
|
+
// contains, so a second path-selection channel alongside it could only disagree with it: a partial commit over
|
|
974
|
+
// a half-staged file records the WORKTREE content while the row the user picked showed the INDEX content. This
|
|
975
|
+
// stages and then records the whole index, which is why it is safe — and why it also survives a merge, where
|
|
976
|
+
// git refuses a partial commit outright (and refuses it only AFTER moving the index).
|
|
973
977
|
export const CommitSchema = RepoParamSchema.extend({
|
|
974
978
|
message: z.string().min(1),
|
|
975
979
|
all: z.boolean().optional(),
|
|
980
|
+
paths: z.array(z.string().min(1)).max(500).optional(),
|
|
976
981
|
});
|
|
977
982
|
export const DiscardSchema = RepoParamSchema.extend({
|
|
978
983
|
// Repo-relative paths to discard; absent ⇒ discard every uncommitted change in the repo.
|
|
@@ -1001,13 +1006,21 @@ export const GitFilesSchema = z.object({ files: z.array(z.string()) });
|
|
|
1001
1006
|
export const GitFileSchema = z.object({ path: z.string(), content: z.string() });
|
|
1002
1007
|
export const CommitResultSchema = z.object({ committed: z.boolean() });
|
|
1003
1008
|
|
|
1009
|
+
// One repo's slice of a workspace-wide git action: the whole repo, or only the repo-relative paths named. The
|
|
1010
|
+
// same pair the per-repo routes take as {repo} + `paths`, in the one shape a caller that spans repos can send.
|
|
1011
|
+
export const RepoPathsSchema = z.object({ repo: z.string().min(1), paths: z.array(z.string().min(1)).max(500).optional() });
|
|
1012
|
+
export type RepoPaths = z.infer<typeof RepoPathsSchema>;
|
|
1013
|
+
|
|
1004
1014
|
/* AI-drafted commit message. Workspace-wide, not per repo, because the commit box's target IS a set of repos
|
|
1005
1015
|
* sharing one message — so the draft has to see every one of their diffs to describe what the commit actually
|
|
1006
|
-
* records.
|
|
1007
|
-
*
|
|
1008
|
-
*
|
|
1016
|
+
* records. The input mirrors CommitSchema field for field, which is the whole point: whatever the commit is
|
|
1017
|
+
* about to do is what gets described, and the two cannot drift.
|
|
1018
|
+
* repos[].paths ⇒ the subset that commit will stage — read the WORKTREE, narrowed to those paths
|
|
1019
|
+
* all: true ⇒ the whole worktree, untracked included (what "Commit all" sweeps)
|
|
1020
|
+
* neither ⇒ the INDEX (what a bare commit records)
|
|
1021
|
+
* Getting that wrong would describe changes the commit isn't going to contain. */
|
|
1009
1022
|
export const CommitMessageDraftSchema = z.object({
|
|
1010
|
-
repos: z.array(
|
|
1023
|
+
repos: z.array(RepoPathsSchema).min(1).max(50),
|
|
1011
1024
|
all: z.boolean().optional(),
|
|
1012
1025
|
});
|
|
1013
1026
|
// The draft plus WHICH model wrote it, so the surface can name it rather than claiming an anonymous "AI" —
|
|
@@ -1316,6 +1329,11 @@ export const WorkspaceChildrenSchema = z.object({
|
|
|
1316
1329
|
});
|
|
1317
1330
|
export type WorkspaceChildren = z.infer<typeof WorkspaceChildrenSchema>;
|
|
1318
1331
|
export const WorkspaceFileQuerySchema = z.object({ path: z.string().min(1) });
|
|
1332
|
+
/* The credential a <video>/<audio> element carries to GET /workspace/media, which is the one workspace route a
|
|
1333
|
+
* browser cannot put a header on. Minted here, over the ordinary bearer-authenticated contract, and scoped to
|
|
1334
|
+
* the single path it was asked for — see auth/media-tickets.ts for why scope rather than single-use is what
|
|
1335
|
+
* bounds it. `expiresAt` is epoch ms so a player can tell a dead ticket from a dead file. */
|
|
1336
|
+
export const WorkspaceMediaTicketSchema = z.object({ ticket: z.string(), expiresAt: z.number() });
|
|
1319
1337
|
/* A text read is a read of a WINDOW: `offset` is the byte to start at (negative reads that many bytes from the
|
|
1320
1338
|
* END, which is what following a growing log means — the tail's offset isn't knowable until the size is), and
|
|
1321
1339
|
* `limit` how many bytes to serve. The daemon clamps `limit` to its own cap, so an omitted or oversized one is
|
|
@@ -2022,23 +2040,13 @@ export const ForticlientConnectionSchema = z.object({
|
|
|
2022
2040
|
export type ForticlientConnection = z.infer<typeof ForticlientConnectionSchema>;
|
|
2023
2041
|
export const ForticlientImportSchema = z.object({ connections: z.array(ForticlientConnectionSchema) });
|
|
2024
2042
|
|
|
2025
|
-
// Browse
|
|
2026
|
-
// optional token for a private
|
|
2043
|
+
// Browse an extension/plugin registry (a git repo with .claude-plugin/marketplace.json — see
|
|
2044
|
+
// @intentic/registry for the format). POST so the optional token for a private registry never rides a URL or
|
|
2045
|
+
// an access log.
|
|
2027
2046
|
export const MarketplaceRequestSchema = z.object({ url: z.string().url(), token: z.string().min(1).optional() });
|
|
2028
|
-
//
|
|
2029
|
-
//
|
|
2030
|
-
export const
|
|
2031
|
-
name: z.string(),
|
|
2032
|
-
description: z.string().optional(),
|
|
2033
|
-
version: z.string().optional(),
|
|
2034
|
-
// "extension" marks an intentic-extension entry (installs as the `extension` capability, sha-pinned);
|
|
2035
|
-
// absent/"plugin" = a Claude Code plugin. Claude Code ignores unknown marketplace fields, so one
|
|
2036
|
-
// marketplace repo serves both consumers.
|
|
2037
|
-
kind: z.enum(["plugin", "extension"]).optional(),
|
|
2038
|
-
install: z.object({ url: z.string(), ref: z.string().optional(), path: z.string().optional() }).optional(),
|
|
2039
|
-
});
|
|
2040
|
-
export type MarketplacePlugin = z.infer<typeof MarketplacePluginSchema>;
|
|
2041
|
-
export const MarketplaceSchema = z.object({ name: z.string(), plugins: z.array(MarketplacePluginSchema) });
|
|
2047
|
+
// The rows are RegistryEntry — the curated decision joined to the resolved pointer and the scanner's upstream
|
|
2048
|
+
// facts, exactly as the site's gallery renders them, so browsing in the app and browsing the web show one list.
|
|
2049
|
+
export const MarketplaceSchema = z.object({ name: z.string(), plugins: z.array(RegistryEntrySchema) });
|
|
2042
2050
|
export type Marketplace = z.infer<typeof MarketplaceSchema>;
|
|
2043
2051
|
|
|
2044
2052
|
// ---- extensions: installed extension-kind capabilities resolved to their manifests ----
|
|
@@ -2194,10 +2202,18 @@ export const WebchatConfigSchema = z.object({
|
|
|
2194
2202
|
// The site's OWN Google OAuth web client id. It cannot be intentic's: Google Identity Services only issues
|
|
2195
2203
|
// a token to an authorized JavaScript origin, and intentic's client can't list every customer domain.
|
|
2196
2204
|
googleClientId: z.string().optional(),
|
|
2197
|
-
|
|
2205
|
+
/* Widget chrome. `position` picks the launcher corner.
|
|
2206
|
+
*
|
|
2207
|
+
* `accent` is a HEX colour, not any CSS colour, because the widget derives values from its channels rather
|
|
2208
|
+
* than just painting it: a glyph step for the scheme, a 14% wash for the send button, a bubble edge, a focus
|
|
2209
|
+
* ring, and the label that goes on top (see webchat-widget's styles.ts). A colour we cannot read is a widget
|
|
2210
|
+
* with half its accent silently missing, so the unreadable case is rejected here instead. */
|
|
2198
2211
|
title: z.string().max(80).optional(),
|
|
2199
2212
|
greeting: z.string().max(500).optional(),
|
|
2200
|
-
accent: z
|
|
2213
|
+
accent: z
|
|
2214
|
+
.string()
|
|
2215
|
+
.regex(/^#([0-9a-fA-F]{3}|[0-9a-fA-F]{6})$/, "accent must be a hex colour, e.g. #e47100")
|
|
2216
|
+
.optional(),
|
|
2201
2217
|
position: z.enum(["top-right", "top-left", "bottom-right", "bottom-left"]).optional(),
|
|
2202
2218
|
/* Two ceilings on top of the route's fixed per-minute window, because a public endpoint's real exposure is
|
|
2203
2219
|
* cost, not request rate: `dailyMessageMax` caps the whole automation per UTC day, `conversationMessageMax`
|
|
@@ -2551,6 +2567,57 @@ export const DeployAlertSchema = z.object({
|
|
|
2551
2567
|
});
|
|
2552
2568
|
export type DeployAlert = z.infer<typeof DeployAlertSchema>;
|
|
2553
2569
|
|
|
2570
|
+
/* Who the API key acts as. Komodo filters EVERY list by the caller's permissions, so a key minted on a service
|
|
2571
|
+
* user with no grants returns 200 and an empty array — byte-identical to a Komodo with nothing deployed. That
|
|
2572
|
+
* ambiguity shipped once and cost a user an afternoon: their Komodo had four stacks and the board said "no
|
|
2573
|
+
* stacks or deployments yet". Carrying the viewer lets the empty state name the actual reason. */
|
|
2574
|
+
export const DeployViewerSchema = z.object({
|
|
2575
|
+
username: z.string(),
|
|
2576
|
+
// Either of Komodo's admin flags — an admin key sees everything, so its empty board really is empty.
|
|
2577
|
+
admin: z.boolean(),
|
|
2578
|
+
});
|
|
2579
|
+
export type DeployViewer = z.infer<typeof DeployViewerSchema>;
|
|
2580
|
+
|
|
2581
|
+
/* A workspace repo that ships a compose file, and the Komodo stack it belongs to.
|
|
2582
|
+
*
|
|
2583
|
+
* Komodo names a stack whatever its creator typed; a repo names its compose project in the file. The two
|
|
2584
|
+
* usually agree, or nearly — `intentic` in the repo against `intentic-platform` in Komodo — so the daemon
|
|
2585
|
+
* SUGGESTS and the owner decides. The link is explicit and persisted because a guess that silently becomes a
|
|
2586
|
+
* fact is worse than no guess: the owner is the one who knows that `atlas` is this repo's staging stack. */
|
|
2587
|
+
export const DeployRepoLinkSchema = z.object({
|
|
2588
|
+
// The workspace repo dir — the same `repo` key the rest of the app joins on.
|
|
2589
|
+
repo: z.string(),
|
|
2590
|
+
// The compose project name: the file's own `name:` when it has one, else the repo dir. This is what
|
|
2591
|
+
// `docker compose up` would call the project, so it is the best guess at the stack's name.
|
|
2592
|
+
projectName: z.string(),
|
|
2593
|
+
// Workspace-relative path of the compose file the name came from, so the UI can say where it looked.
|
|
2594
|
+
composePath: z.string(),
|
|
2595
|
+
// The stack the owner linked, once they have. Absent ⇒ unlinked, and `suggestions` is the offer.
|
|
2596
|
+
linkedStack: z.string().optional(),
|
|
2597
|
+
// Stack names that look like this repo, best first. Empty when nothing resembles it — in which case the
|
|
2598
|
+
// UI offers the full list rather than pretending it has an opinion.
|
|
2599
|
+
suggestions: z.array(z.string()),
|
|
2600
|
+
});
|
|
2601
|
+
export type DeployRepoLink = z.infer<typeof DeployRepoLinkSchema>;
|
|
2602
|
+
|
|
2603
|
+
// Link a repo to a stack, or clear the link with an empty `stack`. Explicit rather than a toggle: the owner
|
|
2604
|
+
// may be replacing one stack with another, and a toggle cannot express that in one call.
|
|
2605
|
+
export const DeployLinkParamSchema = z.object({
|
|
2606
|
+
capability: z.string(),
|
|
2607
|
+
repo: z.string(),
|
|
2608
|
+
stack: z.string(),
|
|
2609
|
+
});
|
|
2610
|
+
export type DeployLinkParam = z.infer<typeof DeployLinkParamSchema>;
|
|
2611
|
+
|
|
2612
|
+
/* EVERY FIELD ADDED AFTER THIS ROUTE FIRST SHIPPED IS OPTIONAL OR DEFAULTED, and that is a rule rather than a
|
|
2613
|
+
* style. The browser validates this response with `.parse()`, and the daemon it is talking to is not
|
|
2614
|
+
* necessarily built from the same commit — a sandbox image is rebuilt on the owner's schedule, the web bundle
|
|
2615
|
+
* on ours. `repos` shipped REQUIRED and the first daemon that predated it took the whole view down with
|
|
2616
|
+
* `Invalid input: expected array, received undefined at repos`: not a missing band, a dead page, on the
|
|
2617
|
+
* surface whose entire job is to tell you whether production is up.
|
|
2618
|
+
*
|
|
2619
|
+
* So: a new field is `.default(...)` when the view can render without it, and `.optional()` when its absence
|
|
2620
|
+
* is itself meaningful. Neither is ever `required`. A newer browser must degrade against an older daemon. */
|
|
2554
2621
|
export const DeployOverviewResponseSchema = z.object({
|
|
2555
2622
|
komodoUrl: z.string(),
|
|
2556
2623
|
// FALSE means Komodo did not answer — the view says so loudly and the badge must not read `danger`.
|
|
@@ -2559,6 +2626,13 @@ export const DeployOverviewResponseSchema = z.object({
|
|
|
2559
2626
|
reachable: z.boolean(),
|
|
2560
2627
|
// Why it did not answer, in Komodo's own words — the view shows it instead of a bare "unavailable".
|
|
2561
2628
|
unreachableReason: z.string().optional(),
|
|
2629
|
+
// Absent when Komodo did not answer — or when the daemon predates the field, which the empty state has to
|
|
2630
|
+
// tell apart from "the key sees nothing", since only one of those is the owner's to fix.
|
|
2631
|
+
viewer: DeployViewerSchema.optional(),
|
|
2632
|
+
// Every workspace repo with a compose file, with its suggested or linked stack. Independent of whether
|
|
2633
|
+
// Komodo returned anything: a repo the owner could link is worth showing even on an empty board, since
|
|
2634
|
+
// that is exactly the moment they need to know what this view is for.
|
|
2635
|
+
repos: z.array(DeployRepoLinkSchema).default([]),
|
|
2562
2636
|
resources: z.array(DeployResourceSchema),
|
|
2563
2637
|
servers: z.array(DeployServerSchema),
|
|
2564
2638
|
// Newest first. Unresolved and resolved both: the view shows recent history, the badge reads only the
|
package/src/workspace-state.ts
CHANGED
|
@@ -83,9 +83,9 @@ export const WORKSPACE_STATE_FILES: readonly WorkspaceStateFile[] = [
|
|
|
83
83
|
why: "Which origins have loaded a Doorbell's widget, written on a 30s flush timer while a customer's site serves page views. The install panel that renders it fetches on open and polls itself while it is on screen, which is the whole window in which the answer changes for anyone. Pushing instead would bill every connected browser a refetch per flush, for a panel almost nobody has open.",
|
|
84
84
|
},
|
|
85
85
|
{
|
|
86
|
-
path: ".intentic/
|
|
86
|
+
path: ".intentic/thread-sessions.json",
|
|
87
87
|
invalidates: [],
|
|
88
|
-
why: "
|
|
88
|
+
why: "Thread bookkeeping (an inbound thread — a Doorbell visitor, a Discord or Slack channel — → sandbox conversation + provider session), written on EVERY inbound message. Nothing in the browser reads it: what a thread produces is a conversation, and the fleet board already learns about that from the agent registry's own push. Naming a key here would bill every connected browser a refetch per inbound message — the request storm this table's own note warns about — to refresh nothing it can see.",
|
|
89
89
|
},
|
|
90
90
|
{
|
|
91
91
|
path: ".intentic/extension-settings.json",
|