@intentic/sandbox-contract 1.126.0 → 1.133.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/agent-catalog.d.ts.map +1 -1
- package/dist/contracts/activity.contract.d.ts +4 -4
- package/dist/contracts/activity.contract.d.ts.map +1 -1
- package/dist/contracts/agent.contract.d.ts +283 -252
- package/dist/contracts/agent.contract.d.ts.map +1 -1
- package/dist/contracts/agent.contract.js +4 -3
- package/dist/contracts/agent.contract.js.map +1 -1
- package/dist/contracts/agents.contract.d.ts +15 -13
- package/dist/contracts/agents.contract.d.ts.map +1 -1
- package/dist/contracts/automations.contract.d.ts +3 -3
- package/dist/contracts/automations.contract.d.ts.map +1 -1
- package/dist/contracts/capabilities.contract.d.ts +21 -16
- package/dist/contracts/capabilities.contract.d.ts.map +1 -1
- package/dist/contracts/claude.contract.d.ts.map +1 -1
- package/dist/contracts/codex.contract.d.ts.map +1 -1
- package/dist/contracts/drafts.contract.d.ts +6 -6
- package/dist/contracts/drafts.contract.d.ts.map +1 -1
- package/dist/contracts/extensions.contract.d.ts +4 -4
- package/dist/contracts/extensions.contract.d.ts.map +1 -1
- package/dist/contracts/git.contract.d.ts +6 -4
- package/dist/contracts/git.contract.d.ts.map +1 -1
- package/dist/contracts/grok.contract.d.ts.map +1 -1
- package/dist/contracts/history.contract.d.ts +3 -3
- package/dist/contracts/history.contract.d.ts.map +1 -1
- package/dist/contracts/intentic.contract.d.ts.map +1 -1
- package/dist/contracts/inventory.contract.d.ts +20 -20
- package/dist/contracts/inventory.contract.d.ts.map +1 -1
- package/dist/contracts/logs.contract.d.ts.map +1 -1
- package/dist/contracts/panels.contract.d.ts +1 -1
- package/dist/contracts/panels.contract.d.ts.map +1 -1
- package/dist/contracts/ports.contract.d.ts +1 -1
- package/dist/contracts/ports.contract.d.ts.map +1 -1
- package/dist/contracts/secrets.contract.d.ts +4 -4
- package/dist/contracts/secrets.contract.d.ts.map +1 -1
- package/dist/contracts/sessions.contract.d.ts +1 -1
- package/dist/contracts/sessions.contract.d.ts.map +1 -1
- package/dist/contracts/settings.contract.d.ts.map +1 -1
- package/dist/contracts/system.contract.d.ts +26 -26
- package/dist/contracts/system.contract.d.ts.map +1 -1
- package/dist/contracts/translator.contract.d.ts.map +1 -1
- package/dist/contracts/workspace.contract.d.ts +21 -21
- package/dist/contracts/workspace.contract.d.ts.map +1 -1
- package/dist/effects.d.ts +1 -1
- package/dist/effects.d.ts.map +1 -1
- package/dist/effects.js +2 -0
- package/dist/effects.js.map +1 -1
- package/dist/events.d.ts +213 -29
- package/dist/events.d.ts.map +1 -1
- package/dist/events.js +5 -0
- package/dist/events.js.map +1 -1
- package/dist/hostnames.d.ts.map +1 -1
- package/dist/index.d.ts +417 -377
- package/dist/index.d.ts.map +1 -1
- package/dist/model-metadata.d.ts.map +1 -1
- package/dist/schemas.d.ts +221 -212
- package/dist/schemas.d.ts.map +1 -1
- package/dist/schemas.js +13 -6
- package/dist/schemas.js.map +1 -1
- package/dist/sse.d.ts.map +1 -1
- package/dist/tunnel-ids.d.ts.map +1 -1
- package/package.json +8 -6
- package/src/agent-catalog.ts +62 -0
- package/src/contracts/activity.contract.ts +9 -0
- package/src/contracts/agent.contract.ts +17 -0
- package/src/contracts/agents.contract.ts +28 -0
- package/src/contracts/automations.contract.ts +23 -0
- package/src/contracts/capabilities.contract.ts +31 -0
- package/src/contracts/claude.contract.ts +22 -0
- package/src/contracts/codex.contract.ts +23 -0
- package/src/contracts/drafts.contract.ts +11 -0
- package/src/contracts/extensions.contract.ts +28 -0
- package/src/contracts/git.contract.ts +64 -0
- package/src/contracts/grok.contract.ts +16 -0
- package/src/contracts/history.contract.ts +12 -0
- package/src/contracts/intentic.contract.ts +17 -0
- package/src/contracts/inventory.contract.ts +11 -0
- package/src/contracts/logs.contract.ts +9 -0
- package/src/contracts/panels.contract.ts +12 -0
- package/src/contracts/ports.contract.ts +11 -0
- package/src/contracts/secrets.contract.ts +15 -0
- package/src/contracts/sessions.contract.ts +14 -0
- package/src/contracts/settings.contract.ts +11 -0
- package/src/contracts/system.contract.ts +37 -0
- package/src/contracts/translator.contract.ts +20 -0
- package/src/contracts/workspace.contract.ts +74 -0
- package/src/effects.test.ts +132 -0
- package/src/effects.ts +138 -0
- package/src/events.ts +277 -0
- package/src/hostnames.test.ts +44 -0
- package/src/hostnames.ts +97 -0
- package/src/index.ts +84 -0
- package/src/model-metadata.ts +30 -0
- package/src/schemas.ts +1407 -0
- package/src/sse.ts +43 -0
- package/src/terminal-protocol.ts +16 -0
- package/src/tunnel-ids.ts +19 -0
package/src/effects.ts
ADDED
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
import type { ConnectorContribution, ExtensionManifest } from "@intentic/extension-api";
|
|
2
|
+
import type { CapabilityKind } from "./schemas.js";
|
|
3
|
+
|
|
4
|
+
/* What adding a capability DOES to the sandbox, as data — the structured counterpart of the handlers' side
|
|
5
|
+
* effects (the sandbox's capabilities/handlers/*), rendered by the web as the "This will add to your sandbox"
|
|
6
|
+
* panel before an add and as per-instance effect strips after. Derived, not declared per card: config-dependent
|
|
7
|
+
* effects (a plugin's clone URL, the SQL card's engine-dependent client image) and connector/extension-declared
|
|
8
|
+
* ones (secret fields, image fragments, processes) are computed from the same contribution data the handlers
|
|
9
|
+
* consume, so there is no per-card effects list to drift. The streamed apply log stays the post-apply record;
|
|
10
|
+
* this is the pre-add disclosure. */
|
|
11
|
+
|
|
12
|
+
export type CapabilityEffect =
|
|
13
|
+
// Writes .claude/skills/<name>/SKILL.md, auto-loaded by the agent next turn. `name` is the instance id for
|
|
14
|
+
// cli/browser (per-instance skills), the fixed shared skill for ssh/vpn; absent while the instance is unnamed.
|
|
15
|
+
| { readonly kind: "skill"; readonly name?: string | undefined }
|
|
16
|
+
// Stores a credential in the sandbox. "agent-env": injected into the agent's environment each turn, never
|
|
17
|
+
// written to a file (cli). "disk": a 0600 file or a denylisted manifest field (ssh key, vpn conf, git token).
|
|
18
|
+
| { readonly kind: "secret"; readonly exposure: "agent-env" | "disk" }
|
|
19
|
+
// Git-clones a repo into .intentic/plugins|extensions/<id>. `url` absent until the form field is filled.
|
|
20
|
+
| { readonly kind: "clone"; readonly url?: string | undefined }
|
|
21
|
+
// Bakes a Dockerfile fragment into the sandbox image overlay — needs a one-time owner rebuild.
|
|
22
|
+
| { readonly kind: "image" }
|
|
23
|
+
// The baked fragment carries a privileged runtime directive: "net-admin" (vpn NET_ADMIN + tun) or
|
|
24
|
+
// "privileged" (docker — the full --privileged run its dockerd needs).
|
|
25
|
+
| { readonly kind: "runtime"; readonly level: "net-admin" | "privileged" }
|
|
26
|
+
// Runs long-lived background processes in the sandbox (an extension's declared processes).
|
|
27
|
+
| { readonly kind: "process"; readonly names: readonly string[] }
|
|
28
|
+
// Registers an mcp__<id>__ server the agent connects to next turn.
|
|
29
|
+
| { readonly kind: "mcp" }
|
|
30
|
+
// Scaffolds workspace repositories. Empty while a name-derived repo is still unnamed.
|
|
31
|
+
| { readonly kind: "scaffold"; readonly repos: readonly string[] }
|
|
32
|
+
// Writes a managed deploy.config.ts entry; `provisions` = also runs the infra apply job now (service).
|
|
33
|
+
| { readonly kind: "deploy"; readonly provisions: boolean }
|
|
34
|
+
// Extension code runs inside the app with the owner's session — the owner-only trust decision.
|
|
35
|
+
| { readonly kind: "trusted-code" }
|
|
36
|
+
// Keeps a logged-in Chromium profile under .intentic/browser/<platform> that the agent drives.
|
|
37
|
+
| { readonly kind: "profile"; readonly platform: string };
|
|
38
|
+
|
|
39
|
+
export interface CapabilityEffectInput {
|
|
40
|
+
readonly kind: CapabilityKind;
|
|
41
|
+
// The instance name — the skill name for cli/browser, the repo name for monorepo.
|
|
42
|
+
readonly id?: string | undefined;
|
|
43
|
+
// Live form values, or a CapabilitySummary's secret-stripped config echo (hasToken/hasSecret booleans).
|
|
44
|
+
readonly config: Record<string, string | number | boolean | undefined>;
|
|
45
|
+
// The cli provider's connector spec — the source of truth for its secret/fragment declarations.
|
|
46
|
+
readonly connector?: ConnectorContribution | undefined;
|
|
47
|
+
// An installed extension's manifest — resolves its process/image contributions (unknowable before install).
|
|
48
|
+
readonly manifest?: ExtensionManifest | undefined;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const filled = (value: string | number | boolean | undefined): boolean => typeof value === "string" && value.length > 0;
|
|
52
|
+
// A token either typed into the form (`token`) or echoed as present on an installed instance (`hasToken`).
|
|
53
|
+
const hasToken = (config: CapabilityEffectInput["config"]): boolean => filled(config["token"]) || config["hasToken"] === true;
|
|
54
|
+
const cloneUrl = (config: CapabilityEffectInput["config"]): string | undefined => (filled(config["url"]) ? String(config["url"]) : undefined);
|
|
55
|
+
|
|
56
|
+
export const capabilityEffects = (input: CapabilityEffectInput): readonly CapabilityEffect[] => {
|
|
57
|
+
switch (input.kind) {
|
|
58
|
+
case "devops":
|
|
59
|
+
return [{ kind: "scaffold", repos: ["intent", "desired-state"] }];
|
|
60
|
+
case "monorepo":
|
|
61
|
+
return [{ kind: "scaffold", repos: input.id === undefined || input.id.length === 0 ? [] : [input.id] }];
|
|
62
|
+
case "mcp": {
|
|
63
|
+
const effects: CapabilityEffect[] = [{ kind: "mcp" }];
|
|
64
|
+
if (hasToken(input.config)) {
|
|
65
|
+
effects.push({ kind: "secret", exposure: "disk" });
|
|
66
|
+
}
|
|
67
|
+
return effects;
|
|
68
|
+
}
|
|
69
|
+
case "service":
|
|
70
|
+
return [{ kind: "deploy", provisions: true }];
|
|
71
|
+
case "integration":
|
|
72
|
+
return [{ kind: "deploy", provisions: false }];
|
|
73
|
+
case "cli": {
|
|
74
|
+
// Without the connector spec (extensions query pending / sandbox unreachable) fall back to the
|
|
75
|
+
// echoed hasSecret — which the web also synthesizes from the card's own secret-marked fields, so
|
|
76
|
+
// the secret row never waits on /extensions. The image row does wait: a connector's fragment
|
|
77
|
+
// (postgres/mysql clients, discord's whisper) is spec data with no static counterpart on the card.
|
|
78
|
+
const effects: CapabilityEffect[] = [{ kind: "skill", name: input.id }];
|
|
79
|
+
const secret =
|
|
80
|
+
input.connector === undefined ? input.config["hasSecret"] === true : input.connector.fields.some((field) => field.secret === true);
|
|
81
|
+
if (secret) {
|
|
82
|
+
effects.push({ kind: "secret", exposure: "agent-env" });
|
|
83
|
+
}
|
|
84
|
+
if (input.connector?.fragment !== undefined) {
|
|
85
|
+
effects.push({ kind: "image" });
|
|
86
|
+
}
|
|
87
|
+
return effects;
|
|
88
|
+
}
|
|
89
|
+
case "plugin": {
|
|
90
|
+
const effects: CapabilityEffect[] = [{ kind: "clone", url: cloneUrl(input.config) }];
|
|
91
|
+
if (hasToken(input.config)) {
|
|
92
|
+
effects.push({ kind: "secret", exposure: "disk" });
|
|
93
|
+
}
|
|
94
|
+
return effects;
|
|
95
|
+
}
|
|
96
|
+
case "extension": {
|
|
97
|
+
const effects: CapabilityEffect[] = [{ kind: "trusted-code" }, { kind: "clone", url: cloneUrl(input.config) }];
|
|
98
|
+
if (hasToken(input.config)) {
|
|
99
|
+
effects.push({ kind: "secret", exposure: "disk" });
|
|
100
|
+
}
|
|
101
|
+
const contributes = input.manifest?.contributes;
|
|
102
|
+
if (contributes?.environment !== undefined) {
|
|
103
|
+
effects.push({ kind: "image" });
|
|
104
|
+
}
|
|
105
|
+
if (contributes?.processes !== undefined && contributes.processes.length > 0) {
|
|
106
|
+
effects.push({ kind: "process", names: contributes.processes.map((process) => process.name) });
|
|
107
|
+
}
|
|
108
|
+
return effects;
|
|
109
|
+
}
|
|
110
|
+
case "ssh":
|
|
111
|
+
return [
|
|
112
|
+
{ kind: "secret", exposure: "disk" },
|
|
113
|
+
{ kind: "skill", name: "ssh" },
|
|
114
|
+
];
|
|
115
|
+
case "vpn":
|
|
116
|
+
return [{ kind: "secret", exposure: "disk" }, { kind: "skill", name: "vpn" }, { kind: "image" }, { kind: "runtime", level: "net-admin" }];
|
|
117
|
+
case "docker":
|
|
118
|
+
// The engine is baked into the base image — the "image" effect here is the overlay rebuild that
|
|
119
|
+
// applies the fragment's --privileged directive, not new tooling.
|
|
120
|
+
return [{ kind: "image" }, { kind: "runtime", level: "privileged" }, { kind: "process", names: ["dockerd"] }];
|
|
121
|
+
case "browser": {
|
|
122
|
+
const effects: CapabilityEffect[] = [{ kind: "skill", name: input.id }, { kind: "image" }];
|
|
123
|
+
const platform = input.config["platform"];
|
|
124
|
+
if (typeof platform === "string") {
|
|
125
|
+
effects.push({ kind: "profile", platform });
|
|
126
|
+
}
|
|
127
|
+
return effects;
|
|
128
|
+
}
|
|
129
|
+
case "agent": {
|
|
130
|
+
// The spawned ACP subprocess is the standing consequence; a pasted env block is a stored credential.
|
|
131
|
+
const effects: CapabilityEffect[] = [{ kind: "process", names: input.id === undefined || input.id.length === 0 ? [] : [input.id] }];
|
|
132
|
+
if (filled(input.config["env"]) || input.config["hasSecret"] === true) {
|
|
133
|
+
effects.push({ kind: "secret", exposure: "disk" });
|
|
134
|
+
}
|
|
135
|
+
return effects;
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
};
|
package/src/events.ts
ADDED
|
@@ -0,0 +1,277 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { AgentSummarySchema } from "./schemas.js";
|
|
3
|
+
|
|
4
|
+
// The wire shapes streamed from the daemon's event-iterator procedures. This is their canonical home: the
|
|
5
|
+
// daemon yields them and the browser client consumes them from the same schema, so the two can't drift (they
|
|
6
|
+
// used to be hand-duplicated across repos). Schemas, not bare types, because oRPC's `eventIterator(...)`
|
|
7
|
+
// validates each frame against them.
|
|
8
|
+
|
|
9
|
+
// One interactive question the agent asks via the `ask` tool (mirrors AskUserQuestion's input shape).
|
|
10
|
+
export const AskOptionSchema = z.object({
|
|
11
|
+
label: z.string(),
|
|
12
|
+
description: z.string(),
|
|
13
|
+
preview: z.string().optional(),
|
|
14
|
+
});
|
|
15
|
+
export type AskOption = z.infer<typeof AskOptionSchema>;
|
|
16
|
+
|
|
17
|
+
export const AskQuestionSchema = z.object({
|
|
18
|
+
question: z.string(),
|
|
19
|
+
header: z.string(),
|
|
20
|
+
multiSelect: z.boolean(),
|
|
21
|
+
options: z.array(AskOptionSchema),
|
|
22
|
+
});
|
|
23
|
+
export type AskQuestion = z.infer<typeof AskQuestionSchema>;
|
|
24
|
+
|
|
25
|
+
// One provider-advertised slash command (an ACP agent's available_commands entry). `hint` is the argument
|
|
26
|
+
// placeholder the popover shows after the name.
|
|
27
|
+
export const AgentCommandSchema = z.object({
|
|
28
|
+
name: z.string(),
|
|
29
|
+
description: z.string(),
|
|
30
|
+
hint: z.string().optional(),
|
|
31
|
+
});
|
|
32
|
+
export type AgentCommand = z.infer<typeof AgentCommandSchema>;
|
|
33
|
+
|
|
34
|
+
// One TodoWrite/Task checklist item, surfaced live so the UI shows the agent's plan-of-work (Claude Code style).
|
|
35
|
+
export const TodoItemSchema = z.object({
|
|
36
|
+
content: z.string(),
|
|
37
|
+
status: z.enum(["pending", "in_progress", "completed"]),
|
|
38
|
+
activeForm: z.string().optional(),
|
|
39
|
+
});
|
|
40
|
+
export type TodoItem = z.infer<typeof TodoItemSchema>;
|
|
41
|
+
|
|
42
|
+
// Claude subscription usage for a turn (the SDK's rate_limit_event): which window is active, how much of it is
|
|
43
|
+
// spent, and when it resets. Emitted on the stream at no token cost; only Claude turns report it. Its own
|
|
44
|
+
// schema so the browser can hold the latest snapshot as a plain value, separate from the `kind`-tagged frame.
|
|
45
|
+
export const RateLimitInfoSchema = z.object({
|
|
46
|
+
status: z.enum(["allowed", "allowed_warning", "rejected"]),
|
|
47
|
+
resetsAt: z.number().optional(), // epoch seconds
|
|
48
|
+
rateLimitType: z.string().optional(), // 'five_hour' | 'seven_day' | 'seven_day_opus' | ...
|
|
49
|
+
utilization: z.number().optional(), // 0-100, how much of the window is used
|
|
50
|
+
});
|
|
51
|
+
export type RateLimitInfo = z.infer<typeof RateLimitInfoSchema>;
|
|
52
|
+
|
|
53
|
+
// Context-window fill for a conversation: how many tokens the latest request sent vs the model's window, so
|
|
54
|
+
// the UI can warn as the chat nears auto-compaction. Per-conversation, unlike the account-wide usage above.
|
|
55
|
+
export const ContextUsageSchema = z.object({
|
|
56
|
+
tokens: z.number(), // full input of the latest request (input + cache read + cache creation)
|
|
57
|
+
contextWindow: z.number(), // the model's context window
|
|
58
|
+
});
|
|
59
|
+
export type ContextUsage = z.infer<typeof ContextUsageSchema>;
|
|
60
|
+
|
|
61
|
+
// ACP-aligned tool taxonomy (Agent Client Protocol's ToolKind, verbatim): what a tool call *does*, driving
|
|
62
|
+
// the card icon and follow-along behavior regardless of which backend named the tool.
|
|
63
|
+
export const ToolKindSchema = z.enum(["read", "edit", "delete", "move", "search", "execute", "think", "fetch", "other"]);
|
|
64
|
+
export type ToolKind = z.infer<typeof ToolKindSchema>;
|
|
65
|
+
|
|
66
|
+
export const ToolCallStatusSchema = z.enum(["pending", "in_progress", "completed", "failed"]);
|
|
67
|
+
export type ToolCallStatus = z.infer<typeof ToolCallStatusSchema>;
|
|
68
|
+
|
|
69
|
+
// A file a tool call touches. Workspace-root-relative, forward-slash (the tree/file route space) — adapters
|
|
70
|
+
// normalize from the turn's cwd. `line` is 1-based.
|
|
71
|
+
export const ToolCallLocationSchema = z.object({
|
|
72
|
+
path: z.string(),
|
|
73
|
+
line: z.number().optional(),
|
|
74
|
+
});
|
|
75
|
+
export type ToolCallLocation = z.infer<typeof ToolCallLocationSchema>;
|
|
76
|
+
|
|
77
|
+
// Structured tool output (ACP's ToolCallContent diff shape, verbatim). `diff` is hunk-level for Edit-style
|
|
78
|
+
// tools (old_string/new_string) and whole-file for Write; an absent oldText means a new file / unknown
|
|
79
|
+
// previous content. Sides are capped daemon-side; `truncated` marks a clipped side.
|
|
80
|
+
export const ToolCallContentSchema = z.discriminatedUnion("type", [
|
|
81
|
+
z.object({ type: z.literal("text"), text: z.string() }),
|
|
82
|
+
z.object({
|
|
83
|
+
type: z.literal("diff"),
|
|
84
|
+
path: z.string(),
|
|
85
|
+
oldText: z.string().optional(),
|
|
86
|
+
newText: z.string(),
|
|
87
|
+
truncated: z.boolean().optional(),
|
|
88
|
+
}),
|
|
89
|
+
]);
|
|
90
|
+
export type ToolCallContent = z.infer<typeof ToolCallContentSchema>;
|
|
91
|
+
|
|
92
|
+
// One frame from an agent turn, relayed to the UI. `kind`-discriminated. The daemon normalizes the SDK's
|
|
93
|
+
// ~40 SDKMessage types down to this union: high-value block types get a dedicated frame
|
|
94
|
+
// (delta/thinking/tool_call/tool_call_update/todos/usage/rate_limit_info/context_usage/init/compact); any SDK message
|
|
95
|
+
// without a UI mapping is dropped. `plan`/`question` pause the turn until the user answers on a side channel.
|
|
96
|
+
// `parentToolUseId` tags frames produced inside a subagent (Task tool).
|
|
97
|
+
export const AgentEventSchema = z.discriminatedUnion("kind", [
|
|
98
|
+
z.object({ kind: z.literal("session"), sessionId: z.string() }),
|
|
99
|
+
// First frame of an isolated turn: the conversation's worktree identity — its branch (agent/<id>) and the
|
|
100
|
+
// ROOT repo's short base sha (the checkout moment). Emitted before any provider frames.
|
|
101
|
+
z.object({ kind: z.literal("worktree"), branch: z.string(), base: z.string() }),
|
|
102
|
+
// Emitted after a clean isolated turn whose delta auto-landed (or failed to): landed ⇒ the work is now
|
|
103
|
+
// UNCOMMITTED changes in the main tree (the Changes panel is the review); conflicts ⇒ it stayed safely in
|
|
104
|
+
// the worktree — the named paths collide with the user's own edits, "Land now" recovers after they resolve.
|
|
105
|
+
z.object({
|
|
106
|
+
kind: z.literal("landed"),
|
|
107
|
+
landed: z.boolean(),
|
|
108
|
+
conflicts: z.array(z.object({ repo: z.string(), paths: z.array(z.string()) })).optional(),
|
|
109
|
+
}),
|
|
110
|
+
// The SDK's init handshake; carries the model it actually resolved for the turn.
|
|
111
|
+
z.object({ kind: z.literal("init"), model: z.string() }),
|
|
112
|
+
// The pre-turn workspace snapshot's id (the attribution-fence "user" capture), emitted once before the
|
|
113
|
+
// provider stream so the client can offer "restore to before this message" on the turn's user bubble.
|
|
114
|
+
// Absent on isolated turns (they snapshot nothing) and when the tree was already clean at turn start.
|
|
115
|
+
z.object({ kind: z.literal("checkpoint"), id: z.string() }),
|
|
116
|
+
z.object({ kind: z.literal("delta"), text: z.string(), parentToolUseId: z.string().optional() }),
|
|
117
|
+
z.object({ kind: z.literal("thinking"), text: z.string(), parentToolUseId: z.string().optional() }),
|
|
118
|
+
// A tool call starting (or, for backends that only report completions, arriving whole). `content` carries
|
|
119
|
+
// structured output known at call time — an Edit's diff is derived from its input, no result needed.
|
|
120
|
+
z.object({
|
|
121
|
+
kind: z.literal("tool_call"),
|
|
122
|
+
id: z.string(),
|
|
123
|
+
name: z.string(),
|
|
124
|
+
category: ToolKindSchema,
|
|
125
|
+
status: ToolCallStatusSchema,
|
|
126
|
+
target: z.string().optional(),
|
|
127
|
+
locations: z.array(ToolCallLocationSchema).optional(),
|
|
128
|
+
content: z.array(ToolCallContentSchema).optional(),
|
|
129
|
+
parentToolUseId: z.string().optional(),
|
|
130
|
+
}),
|
|
131
|
+
// A later state of a tool call, correlated by `id`. N updates per call: status transitions and/or fresh
|
|
132
|
+
// content/locations — both REPLACE the prior value (snapshot semantics, not append); absent ⇒ unchanged.
|
|
133
|
+
z.object({
|
|
134
|
+
kind: z.literal("tool_call_update"),
|
|
135
|
+
id: z.string(),
|
|
136
|
+
status: ToolCallStatusSchema.optional(),
|
|
137
|
+
content: z.array(ToolCallContentSchema).optional(),
|
|
138
|
+
locations: z.array(ToolCallLocationSchema).optional(),
|
|
139
|
+
}),
|
|
140
|
+
// The agent just started running Bash in its live `agent-<id>` tmux session — the client surfaces that
|
|
141
|
+
// terminal in the global panel. One per turn (the session is reused across a turn's commands, incl. subagents').
|
|
142
|
+
z.object({ kind: z.literal("terminal"), session: z.string() }),
|
|
143
|
+
z.object({ kind: z.literal("todos"), items: z.array(TodoItemSchema) }),
|
|
144
|
+
// The provider's own slash commands (ACP available_commands_update), replaced whole each time — the
|
|
145
|
+
// composer's `/` popover lists them; invoking one is plain `/name …` prompt text (the ACP convention).
|
|
146
|
+
z.object({ kind: z.literal("commands"), items: z.array(AgentCommandSchema) }),
|
|
147
|
+
z.object({
|
|
148
|
+
kind: z.literal("usage"),
|
|
149
|
+
// The account that served this turn — the client attributes the totals to it (tagged by streamAgent).
|
|
150
|
+
account: z.string().optional(),
|
|
151
|
+
costUsd: z.number().optional(),
|
|
152
|
+
inputTokens: z.number().optional(),
|
|
153
|
+
outputTokens: z.number().optional(),
|
|
154
|
+
// Provider prompt-cache buckets for the turn: tokens served from cache (read) and written to cache
|
|
155
|
+
// (creation). Optional per provider — Codex reports only cached input (read); runtimes/turns that
|
|
156
|
+
// don't report a bucket omit it. Lets the client show cache hit rate = read / (read + input).
|
|
157
|
+
cacheReadTokens: z.number().optional(),
|
|
158
|
+
cacheCreationTokens: z.number().optional(),
|
|
159
|
+
durationMs: z.number().optional(),
|
|
160
|
+
numTurns: z.number().optional(),
|
|
161
|
+
}),
|
|
162
|
+
// account tags which Claude account the snapshot belongs to, so the client keys usageStatus by account.
|
|
163
|
+
RateLimitInfoSchema.extend({ kind: z.literal("rate_limit_info"), account: z.string().optional() }),
|
|
164
|
+
ContextUsageSchema.extend({ kind: z.literal("context_usage") }),
|
|
165
|
+
z.object({ kind: z.literal("compact"), trigger: z.string(), preTokens: z.number().optional(), postTokens: z.number().optional() }),
|
|
166
|
+
z.object({ kind: z.literal("plan"), decisionId: z.string(), text: z.string() }),
|
|
167
|
+
z.object({ kind: z.literal("question"), requestId: z.string(), questions: z.array(AskQuestionSchema) }),
|
|
168
|
+
// `code` is a machine-readable discriminator for errors the UI reacts to programmatically (dropping a
|
|
169
|
+
// dead session id so the next send self-heals). Absent on plain failures.
|
|
170
|
+
z.object({
|
|
171
|
+
kind: z.literal("error"),
|
|
172
|
+
message: z.string(),
|
|
173
|
+
code: z
|
|
174
|
+
.enum([
|
|
175
|
+
"session-not-found",
|
|
176
|
+
"rate_limit",
|
|
177
|
+
"codex-reauth",
|
|
178
|
+
"grok-model-invalid",
|
|
179
|
+
"codex-model-invalid",
|
|
180
|
+
"subscription-required",
|
|
181
|
+
"agent-busy",
|
|
182
|
+
])
|
|
183
|
+
.optional(),
|
|
184
|
+
}),
|
|
185
|
+
z.object({ kind: z.literal("done") }),
|
|
186
|
+
]);
|
|
187
|
+
export type AgentEvent = z.infer<typeof AgentEventSchema>;
|
|
188
|
+
|
|
189
|
+
// The /agent/attach stream: a head frame identifying the run, then its AgentEvents stamped with their 1-based
|
|
190
|
+
// seq (the client's resume cursor), then `end` when the run is over — every frame delivered, nothing more
|
|
191
|
+
// coming. A stream that closes WITHOUT `end` was dropped mid-run; the client re-attaches with `after` = the
|
|
192
|
+
// last seq it holds. The head's `prompt`/`startedAt` let a window that didn't initiate the turn (a reload, a
|
|
193
|
+
// second window, another device) synthesize the user bubble and the elapsed readout; its `seq` is the log
|
|
194
|
+
// length at attach time — the replay/live boundary.
|
|
195
|
+
export const AttachFrameSchema = z.discriminatedUnion("kind", [
|
|
196
|
+
z.object({ kind: z.literal("attached"), run: z.string(), prompt: z.string(), startedAt: z.number(), seq: z.number() }),
|
|
197
|
+
z.object({ kind: z.literal("frame"), seq: z.number(), event: AgentEventSchema }),
|
|
198
|
+
z.object({ kind: z.literal("end") }),
|
|
199
|
+
]);
|
|
200
|
+
export type AttachFrame = z.infer<typeof AttachFrameSchema>;
|
|
201
|
+
|
|
202
|
+
// One parsed line from `intentic … --output ndjson` (engine events, provider `log`, the terminal `result`).
|
|
203
|
+
// Open-ended by design — the sandbox consumes the wire shape, not @intentic/engine's types — so a string
|
|
204
|
+
// `kind` plus arbitrary extra fields pass through. The apply-events tail (intentic.contract `applyEvents`) rides
|
|
205
|
+
// this same loose shape with three daemon/CLI-minted sentinel kinds alongside the engine ones: {kind:"start"}
|
|
206
|
+
// (first line, written when the run's file is reset), {kind:"exit",code} (last line, on the CLI process exit),
|
|
207
|
+
// and {kind:"heartbeat"} (interleaved by the tail while idle to keep the held-open stream alive).
|
|
208
|
+
export const IntenticLineSchema = z.looseObject({ kind: z.string() });
|
|
209
|
+
export type IntenticLine = z.infer<typeof IntenticLineSchema>;
|
|
210
|
+
|
|
211
|
+
// The daemon's liveness heartbeat frame: the browser holds the events stream open and trips a watchdog if the
|
|
212
|
+
// frames stop (the tunnel drops the proxied response when the origin dies).
|
|
213
|
+
export const HeartbeatSchema = z.object({ kind: z.literal("heartbeat") });
|
|
214
|
+
export type Heartbeat = z.infer<typeof HeartbeatSchema>;
|
|
215
|
+
|
|
216
|
+
// The stream's first frame: the workspace's stable identity, minted at the first boot of an empty /work. The
|
|
217
|
+
// browser remembers it per sandbox id and drops that sandbox's persisted query cache when it changes — a wiped
|
|
218
|
+
// and recreated workspace (cleanup.sh + reconnect keeps the same sandbox id) must not be painted from the
|
|
219
|
+
// previous workspace's cache.
|
|
220
|
+
export const HelloSchema = z.object({ kind: z.literal("hello"), workspaceId: z.string() });
|
|
221
|
+
export type Hello = z.infer<typeof HelloSchema>;
|
|
222
|
+
|
|
223
|
+
// The FULL discovered repo set (sorted root-relative ids), pushed whenever it changes — a clone, a scaffold,
|
|
224
|
+
// or a deleted repo re-frames it. The watcher descent-ignores .git, so no workspaceChanged path pattern can
|
|
225
|
+
// detect a repo appearing; the daemon diffs its own discovery instead. Snapshot-not-diff, last frame wins.
|
|
226
|
+
export const ReposChangedSchema = z.object({ kind: z.literal("reposChanged"), repos: z.array(z.string()) });
|
|
227
|
+
export type ReposChanged = z.infer<typeof ReposChangedSchema>;
|
|
228
|
+
|
|
229
|
+
// A batch of workspace paths that just changed on disk (created/edited/deleted), pushed on the same /events
|
|
230
|
+
// stream as the heartbeat so the browser refreshes the tree + any open file live — the agent edits files
|
|
231
|
+
// out-of-band (its own Write/Edit/Bash tools), so there's no HTTP mutation to hang an invalidate on. Paths are
|
|
232
|
+
// root-relative, forward-slash (the tree/file route space). An empty array means "something changed, refetch the
|
|
233
|
+
// tree" — a burst too large to enumerate, or a reconnect recovery where we don't know what was missed.
|
|
234
|
+
export const WorkspaceChangedSchema = z.object({ kind: z.literal("workspaceChanged"), paths: z.array(z.string()) });
|
|
235
|
+
export type WorkspaceChanged = z.infer<typeof WorkspaceChangedSchema>;
|
|
236
|
+
|
|
237
|
+
// One connected browser tab of a sandbox member. Identity fields come from the caller's verified Google ID
|
|
238
|
+
// token; activity fields from the tab's own /system/presence reports. No timestamps on the wire — an entry's
|
|
239
|
+
// lifetime IS its /events connection's lifetime, so there is nothing to age out or compare clocks over.
|
|
240
|
+
export const PresenceUserSchema = z.object({
|
|
241
|
+
// Per-CONNECTION id, minted by the browser for each /events attempt — never reused across reconnects.
|
|
242
|
+
clientId: z.string(),
|
|
243
|
+
email: z.string(),
|
|
244
|
+
name: z.string().optional(),
|
|
245
|
+
picture: z.string().optional(),
|
|
246
|
+
idle: z.boolean(),
|
|
247
|
+
// Route/view name the tab is on ("workspace", "automations", "ext:<id>/<key>", …).
|
|
248
|
+
view: z.string().optional(),
|
|
249
|
+
// The chat conversation the tab has active.
|
|
250
|
+
sessionId: z.string().optional(),
|
|
251
|
+
// The workspace file the tab has open (root-relative, forward-slash).
|
|
252
|
+
path: z.string().optional(),
|
|
253
|
+
});
|
|
254
|
+
export type PresenceUser = z.infer<typeof PresenceUserSchema>;
|
|
255
|
+
|
|
256
|
+
// The FULL roster of connected members, broadcast on every change — snapshots, not diffs, so a reconnecting
|
|
257
|
+
// browser is consistent from its first frame and ordering never matters (last frame wins).
|
|
258
|
+
export const PresenceSchema = z.object({ kind: z.literal("presence"), users: z.array(PresenceUserSchema) });
|
|
259
|
+
export type Presence = z.infer<typeof PresenceSchema>;
|
|
260
|
+
|
|
261
|
+
// The FULL fleet roster, broadcast on every registry change — same snapshot-not-diff contract as presence:
|
|
262
|
+
// a reconnecting browser is consistent from its first frame and last frame wins.
|
|
263
|
+
export const AgentsSchema = z.object({ kind: z.literal("agents"), agents: z.array(AgentSummarySchema) });
|
|
264
|
+
export type Agents = z.infer<typeof AgentsSchema>;
|
|
265
|
+
|
|
266
|
+
// The /events stream union: the hello identity frame, then liveness heartbeats interleaved with
|
|
267
|
+
// workspace-change batches, repo-set snapshots, and presence + fleet roster snapshots. oRPC validates every
|
|
268
|
+
// yielded frame against this, so all kinds must live here.
|
|
269
|
+
export const SystemEventSchema = z.discriminatedUnion("kind", [
|
|
270
|
+
HelloSchema,
|
|
271
|
+
HeartbeatSchema,
|
|
272
|
+
WorkspaceChangedSchema,
|
|
273
|
+
ReposChangedSchema,
|
|
274
|
+
PresenceSchema,
|
|
275
|
+
AgentsSchema,
|
|
276
|
+
]);
|
|
277
|
+
export type SystemEvent = z.infer<typeof SystemEventSchema>;
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { expect, test } from "vitest";
|
|
2
|
+
import {
|
|
3
|
+
labelHostname,
|
|
4
|
+
panelFromHost,
|
|
5
|
+
portHostname,
|
|
6
|
+
portLabel,
|
|
7
|
+
portSlotFromHost,
|
|
8
|
+
portUrl,
|
|
9
|
+
previewHostname,
|
|
10
|
+
previewLabel,
|
|
11
|
+
previewUrl,
|
|
12
|
+
} from "./hostnames.js";
|
|
13
|
+
|
|
14
|
+
const ID = "abc123def456";
|
|
15
|
+
|
|
16
|
+
test("preview and port hostnames round-trip through their Host-header parsers", () => {
|
|
17
|
+
expect(panelFromHost(`${previewHostname("shop--web", ID, "example.com")}:443`, ID)).toBe("shop--web");
|
|
18
|
+
expect(portSlotFromHost(`${portHostname("a", ID, "example.com")}:443`, ID)).toBe("a");
|
|
19
|
+
// The id-less form (loopback tests / provider-deployed workspaces fronting the proxy themselves).
|
|
20
|
+
expect(panelFromHost("preview-app.example.com", undefined)).toBe("app");
|
|
21
|
+
expect(portSlotFromHost("port-a.example.com", undefined)).toBe("a");
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
test("the two schemes never bleed into each other, and stray/wrong-id hosts parse to nothing", () => {
|
|
25
|
+
expect(portSlotFromHost("preview-app-" + ID + ".example.com", ID)).toBeUndefined();
|
|
26
|
+
expect(panelFromHost("port-a-" + ID + ".example.com", ID)).toBeUndefined();
|
|
27
|
+
expect(panelFromHost("app.example.com", ID)).toBeUndefined();
|
|
28
|
+
expect(panelFromHost(`preview-app-000000000000.example.com`, ID)).toBeUndefined();
|
|
29
|
+
expect(portSlotFromHost(`port-a-000000000000.example.com`, ID)).toBeUndefined();
|
|
30
|
+
expect(panelFromHost(undefined, ID)).toBeUndefined();
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
test("labels are the mintable unit: hostname = <label>-<id>.<zone> for both schemes", () => {
|
|
34
|
+
expect(labelHostname(previewLabel("app"), ID, "example.com")).toBe(previewHostname("app", ID, "example.com"));
|
|
35
|
+
expect(labelHostname(portLabel("a"), ID, "example.com")).toBe(portHostname("a", ID, "example.com"));
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
test("preview/port URLs require both a zone and a sandbox id", () => {
|
|
39
|
+
expect(previewUrl("app", "example.com", ID)).toBe(`https://preview-app-${ID}.example.com`);
|
|
40
|
+
expect(portUrl("a", "example.com", ID)).toBe(`https://port-a-${ID}.example.com`);
|
|
41
|
+
expect(portUrl("a", undefined, ID)).toBeUndefined();
|
|
42
|
+
expect(portUrl("a", "", ID)).toBeUndefined();
|
|
43
|
+
expect(portUrl("a", "example.com", undefined)).toBeUndefined();
|
|
44
|
+
});
|
package/src/hostnames.ts
ADDED
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
// The single source for the tunnel/preview hostname + ingress scheme, shared by the CLI, platform API, daemon,
|
|
2
|
+
// AND the browser. Pure string builders/parsers with NO node imports (unlike ./tunnel-ids, which needs
|
|
3
|
+
// node:crypto for the digest) — so the web bundle can import them and derive identical names. The caller supplies
|
|
4
|
+
// the 12-hex id (sandboxIdFromToken/hostSshIdFromToken in node; WebCrypto in the browser).
|
|
5
|
+
//
|
|
6
|
+
// All four apps MUST agree on these strings: a divergence resolves to NXDOMAIN that resolvers negative-cache for
|
|
7
|
+
// the zone's SOA TTL. This module is that agreement.
|
|
8
|
+
|
|
9
|
+
// The sandbox daemon's subdomain + hostname: `sandbox-<id>` / `sandbox-<id>.<zone>`.
|
|
10
|
+
export const sandboxSubdomain = (id: string): string => `sandbox-${id}`;
|
|
11
|
+
export const sandboxHostname = (id: string, zone: string): string => `${sandboxSubdomain(id)}.${zone}`;
|
|
12
|
+
|
|
13
|
+
// The container sshd hostname the desktop-sync (Mutagen) reaches over the sandbox tunnel: `ssh-<id>.<zone>`.
|
|
14
|
+
export const sshHostname = (id: string, zone: string): string => `ssh-${id}.${zone}`;
|
|
15
|
+
|
|
16
|
+
// A per-host SSH tunnel's Cloudflare tunnel NAME (its hostname reuses sshHostname with the host-ssh id).
|
|
17
|
+
export const hostSshTunnelName = (id: string): string => `host-ssh-${id}`;
|
|
18
|
+
|
|
19
|
+
// The proxied-CNAME target every tunnel points its DNS record at.
|
|
20
|
+
export const cfargotunnelCname = (tunnelId: string): string => `${tunnelId}.cfargotunnel.com`;
|
|
21
|
+
|
|
22
|
+
// The cloudflared ingress catch-all — must be the LAST rule.
|
|
23
|
+
export const CATCH_ALL = { service: "http_status:404" } as const;
|
|
24
|
+
|
|
25
|
+
// Preview scheme: `preview-<panel>-<sandboxId>.<zone>` — one DNS label (the free Universal SSL `*.<zone>` cert
|
|
26
|
+
// covers exactly one level), where <panel> is `<repo>` or `<repo>--<app>` and <sandboxId> pins the hostname to
|
|
27
|
+
// this sandbox (the shared intentic zone hosts many sandboxes; without the id two users' panels would collide).
|
|
28
|
+
// Port-forward scheme: `port-<slot>-<sandboxId>.<zone>` — the same shape with a `port-` prefix, where <slot>
|
|
29
|
+
// is one of the sandbox's fixed forward slots (see PORT_SLOTS), not the port number itself: slots keep the
|
|
30
|
+
// intentic-provided path's minted routes bounded and warm while dev servers churn ephemeral ports.
|
|
31
|
+
//
|
|
32
|
+
// A *label* is the first-DNS-label prefix before `-<sandboxId>` (`preview-<panel>` / `port-<slot>`) — the unit
|
|
33
|
+
// the platform's /sandbox/preview-route mints, so one endpoint serves both schemes.
|
|
34
|
+
export const previewLabel = (panel: string): string => `preview-${panel}`;
|
|
35
|
+
export const portLabel = (slot: string): string => `port-${slot}`;
|
|
36
|
+
|
|
37
|
+
// The fixed per-sandbox forward slots. Eight is deliberate: enough for a monorepo's worth of concurrent dev
|
|
38
|
+
// servers, and the hard cap on preview DNS records a sandbox can ever cost the shared intentic zone.
|
|
39
|
+
export const PORT_SLOTS = ["a", "b", "c", "d", "e", "f", "g", "h"] as const;
|
|
40
|
+
|
|
41
|
+
// The hostname a label resolves to — what the platform's /sandbox/preview-route mints from the label alone.
|
|
42
|
+
export const labelHostname = (label: string, id: string, zone: string): string => `${label}-${id}.${zone}`;
|
|
43
|
+
export const previewHostname = (panel: string, id: string, zone: string): string => labelHostname(previewLabel(panel), id, zone);
|
|
44
|
+
export const portHostname = (slot: string, id: string, zone: string): string => labelHostname(portLabel(slot), id, zone);
|
|
45
|
+
|
|
46
|
+
// A panel's / forwarded port's preview URL — undefined unless the sandbox has both a zone and an id
|
|
47
|
+
// (headless/loopback sandboxes have neither and advertise no preview).
|
|
48
|
+
export const previewUrl = (panel: string, zone: string | undefined, sandboxId: string | undefined): string | undefined =>
|
|
49
|
+
zone !== undefined && zone !== "" && sandboxId !== undefined ? `https://${previewHostname(panel, sandboxId, zone)}` : undefined;
|
|
50
|
+
export const portUrl = (slot: string, zone: string | undefined, sandboxId: string | undefined): string | undefined =>
|
|
51
|
+
zone !== undefined && zone !== "" && sandboxId !== undefined ? `https://${portHostname(slot, sandboxId, zone)}` : undefined;
|
|
52
|
+
|
|
53
|
+
// The key after `<prefix>` from a request's Host header. The first DNS label must carry the prefix (the
|
|
54
|
+
// own-Cloudflare wildcard also catches stray subdomains → undefined → the caller's 404) and, when the sandbox
|
|
55
|
+
// has an id, the exact `-<sandboxId>` suffix — a fixed-length match, so keys containing `-` stay unambiguous.
|
|
56
|
+
// Without an id the bare label is the key (loopback tests and provider-deployed workspaces, which front the
|
|
57
|
+
// proxy themselves).
|
|
58
|
+
const keyFromHost = (prefix: string, hostHeader: string | undefined, sandboxId: string | undefined): string | undefined => {
|
|
59
|
+
const label = hostHeader?.split(":")[0]?.split(".")[0] ?? "";
|
|
60
|
+
if (!label.startsWith(prefix)) {
|
|
61
|
+
return undefined;
|
|
62
|
+
}
|
|
63
|
+
const key = label.slice(prefix.length);
|
|
64
|
+
if (sandboxId === undefined) {
|
|
65
|
+
return key === "" ? undefined : key;
|
|
66
|
+
}
|
|
67
|
+
const suffix = `-${sandboxId}`;
|
|
68
|
+
return key.length > suffix.length && key.endsWith(suffix) ? key.slice(0, -suffix.length) : undefined;
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
export const panelFromHost = (hostHeader: string | undefined, sandboxId: string | undefined): string | undefined =>
|
|
72
|
+
keyFromHost("preview-", hostHeader, sandboxId);
|
|
73
|
+
export const portSlotFromHost = (hostHeader: string | undefined, sandboxId: string | undefined): string | undefined =>
|
|
74
|
+
keyFromHost("port-", hostHeader, sandboxId);
|
|
75
|
+
|
|
76
|
+
// The Cloudflare zone from a sandbox public URL (https://sandbox-<id>.<zone> → <zone>): the hostname minus its
|
|
77
|
+
// first DNS label. undefined when the URL is unparsable OR the hostname has fewer than three labels (no zone
|
|
78
|
+
// suffix to strip — e.g. a 2-label host would otherwise yield a bare TLD). Accepts scheme-less input too, so it
|
|
79
|
+
// works whether the caller passes `https://…` (daemon/CLI) or a bare host. This is the single reconciled
|
|
80
|
+
// implementation of what used to be the daemon's `zoneFromPublicUrl` and the web's `zoneFromDaemonUrl`.
|
|
81
|
+
export const zoneFromUrl = (url: string | undefined): string | undefined => {
|
|
82
|
+
if (url === undefined || url === "") {
|
|
83
|
+
return undefined;
|
|
84
|
+
}
|
|
85
|
+
const withScheme = /^[a-z][a-z0-9+.-]*:\/\//i.test(url) ? url : `https://${url}`;
|
|
86
|
+
let hostname: string;
|
|
87
|
+
try {
|
|
88
|
+
hostname = new URL(withScheme).hostname;
|
|
89
|
+
} catch {
|
|
90
|
+
return undefined;
|
|
91
|
+
}
|
|
92
|
+
const labels = hostname.split(".");
|
|
93
|
+
if (labels.length < 3) {
|
|
94
|
+
return undefined;
|
|
95
|
+
}
|
|
96
|
+
return labels.slice(1).join(".");
|
|
97
|
+
};
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import { activityContract } from "./contracts/activity.contract.js";
|
|
2
|
+
import { agentContract } from "./contracts/agent.contract.js";
|
|
3
|
+
import { agentsContract } from "./contracts/agents.contract.js";
|
|
4
|
+
import { automationsContract } from "./contracts/automations.contract.js";
|
|
5
|
+
import { capabilitiesContract } from "./contracts/capabilities.contract.js";
|
|
6
|
+
import { claudeContract } from "./contracts/claude.contract.js";
|
|
7
|
+
import { codexContract } from "./contracts/codex.contract.js";
|
|
8
|
+
import { draftsContract } from "./contracts/drafts.contract.js";
|
|
9
|
+
import { extensionsContract } from "./contracts/extensions.contract.js";
|
|
10
|
+
import { gitContract } from "./contracts/git.contract.js";
|
|
11
|
+
import { grokContract } from "./contracts/grok.contract.js";
|
|
12
|
+
import { historyContract } from "./contracts/history.contract.js";
|
|
13
|
+
import { intenticContract } from "./contracts/intentic.contract.js";
|
|
14
|
+
import { inventoryContract } from "./contracts/inventory.contract.js";
|
|
15
|
+
import { logsContract } from "./contracts/logs.contract.js";
|
|
16
|
+
import { panelsContract } from "./contracts/panels.contract.js";
|
|
17
|
+
import { portsContract } from "./contracts/ports.contract.js";
|
|
18
|
+
import { secretsContract } from "./contracts/secrets.contract.js";
|
|
19
|
+
import { sessionsContract } from "./contracts/sessions.contract.js";
|
|
20
|
+
import { settingsContract } from "./contracts/settings.contract.js";
|
|
21
|
+
import { systemContract } from "./contracts/system.contract.js";
|
|
22
|
+
import { translatorContract } from "./contracts/translator.contract.js";
|
|
23
|
+
import { workspaceContract } from "./contracts/workspace.contract.js";
|
|
24
|
+
|
|
25
|
+
export { activityContract } from "./contracts/activity.contract.js";
|
|
26
|
+
export { agentContract } from "./contracts/agent.contract.js";
|
|
27
|
+
export { agentsContract } from "./contracts/agents.contract.js";
|
|
28
|
+
export { automationsContract } from "./contracts/automations.contract.js";
|
|
29
|
+
export { capabilitiesContract } from "./contracts/capabilities.contract.js";
|
|
30
|
+
export { claudeContract } from "./contracts/claude.contract.js";
|
|
31
|
+
export { codexContract } from "./contracts/codex.contract.js";
|
|
32
|
+
export { draftsContract } from "./contracts/drafts.contract.js";
|
|
33
|
+
export { extensionsContract } from "./contracts/extensions.contract.js";
|
|
34
|
+
export { gitContract } from "./contracts/git.contract.js";
|
|
35
|
+
export { grokContract } from "./contracts/grok.contract.js";
|
|
36
|
+
export { historyContract } from "./contracts/history.contract.js";
|
|
37
|
+
export { intenticContract } from "./contracts/intentic.contract.js";
|
|
38
|
+
export { inventoryContract } from "./contracts/inventory.contract.js";
|
|
39
|
+
export { logsContract } from "./contracts/logs.contract.js";
|
|
40
|
+
export { panelsContract } from "./contracts/panels.contract.js";
|
|
41
|
+
export { portsContract } from "./contracts/ports.contract.js";
|
|
42
|
+
export { secretsContract } from "./contracts/secrets.contract.js";
|
|
43
|
+
export { sessionsContract } from "./contracts/sessions.contract.js";
|
|
44
|
+
export { settingsContract } from "./contracts/settings.contract.js";
|
|
45
|
+
export { systemContract } from "./contracts/system.contract.js";
|
|
46
|
+
export { translatorContract } from "./contracts/translator.contract.js";
|
|
47
|
+
export { workspaceContract } from "./contracts/workspace.contract.js";
|
|
48
|
+
export * from "./effects.js";
|
|
49
|
+
export * from "./events.js";
|
|
50
|
+
export * from "./sse.js";
|
|
51
|
+
export * from "./agent-catalog.js";
|
|
52
|
+
export * from "./hostnames.js";
|
|
53
|
+
export * from "./model-metadata.js";
|
|
54
|
+
export * from "./schemas.js";
|
|
55
|
+
export * from "./terminal-protocol.js";
|
|
56
|
+
|
|
57
|
+
// The aggregated contract — implemented on the server by the per-domain route factories and consumed by the
|
|
58
|
+
// browser's typed oRPC client (ContractRouterClient<typeof sandboxContract>). The wire paths it declares are
|
|
59
|
+
// mounted at the sandbox root, so /health and /workspace/raw (plain Hono routes) sit alongside it.
|
|
60
|
+
export const sandboxContract = {
|
|
61
|
+
activity: activityContract,
|
|
62
|
+
agent: agentContract,
|
|
63
|
+
agents: agentsContract,
|
|
64
|
+
automations: automationsContract,
|
|
65
|
+
capabilities: capabilitiesContract,
|
|
66
|
+
claude: claudeContract,
|
|
67
|
+
codex: codexContract,
|
|
68
|
+
drafts: draftsContract,
|
|
69
|
+
extensions: extensionsContract,
|
|
70
|
+
sessions: sessionsContract,
|
|
71
|
+
settings: settingsContract,
|
|
72
|
+
intentic: intenticContract,
|
|
73
|
+
git: gitContract,
|
|
74
|
+
grok: grokContract,
|
|
75
|
+
history: historyContract,
|
|
76
|
+
workspace: workspaceContract,
|
|
77
|
+
inventory: inventoryContract,
|
|
78
|
+
logs: logsContract,
|
|
79
|
+
panels: panelsContract,
|
|
80
|
+
ports: portsContract,
|
|
81
|
+
secrets: secretsContract,
|
|
82
|
+
system: systemContract,
|
|
83
|
+
translator: translatorContract,
|
|
84
|
+
};
|