@intentic/sandbox-contract 1.316.0 → 1.317.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/contracts/agent.contract.d.ts +561 -0
- package/dist/contracts/agent.contract.d.ts.map +1 -1
- package/dist/contracts/agents.contract.d.ts +402 -0
- package/dist/contracts/agents.contract.d.ts.map +1 -1
- package/dist/contracts/capabilities.contract.d.ts +16 -0
- package/dist/contracts/capabilities.contract.d.ts.map +1 -1
- package/dist/contracts/capabilities.contract.js +10 -1
- package/dist/contracts/capabilities.contract.js.map +1 -1
- package/dist/contracts/device.contract.d.ts +1 -0
- package/dist/contracts/device.contract.d.ts.map +1 -1
- package/dist/contracts/needs.contract.d.ts +532 -0
- package/dist/contracts/needs.contract.d.ts.map +1 -0
- package/dist/contracts/needs.contract.js +80 -0
- package/dist/contracts/needs.contract.js.map +1 -0
- package/dist/contracts/runner.contract.d.ts +294 -122
- package/dist/contracts/runner.contract.d.ts.map +1 -1
- package/dist/contracts/secrets.contract.d.ts +16 -0
- package/dist/contracts/secrets.contract.d.ts.map +1 -1
- package/dist/contracts/secrets.contract.js +12 -2
- package/dist/contracts/secrets.contract.js.map +1 -1
- package/dist/contracts/sessions.contract.d.ts +79 -0
- package/dist/contracts/sessions.contract.d.ts.map +1 -1
- package/dist/contracts/settings.contract.d.ts +2 -0
- package/dist/contracts/settings.contract.d.ts.map +1 -1
- package/dist/contracts/system.contract.d.ts +160 -0
- package/dist/contracts/system.contract.d.ts.map +1 -1
- package/dist/contracts/system.contract.js +11 -0
- package/dist/contracts/system.contract.js.map +1 -1
- package/dist/events/agent-events.d.ts +309 -0
- package/dist/events/agent-events.d.ts.map +1 -1
- package/dist/events/agent-events.js +2 -0
- package/dist/events/agent-events.js.map +1 -1
- package/dist/events/agent-words.d.ts.map +1 -1
- package/dist/events/agent-words.js +2 -1
- package/dist/events/agent-words.js.map +1 -1
- package/dist/events/need-wake.d.ts +17 -0
- package/dist/events/need-wake.d.ts.map +1 -0
- package/dist/events/need-wake.js +34 -0
- package/dist/events/need-wake.js.map +1 -0
- package/dist/events/system-events.d.ts +38 -0
- package/dist/events/system-events.d.ts.map +1 -1
- package/dist/events/transcript.d.ts +484 -0
- package/dist/events/transcript.d.ts.map +1 -1
- package/dist/events/transcript.js +9 -0
- package/dist/events/transcript.js.map +1 -1
- package/dist/index.d.ts +1826 -55
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +6 -0
- package/dist/index.js.map +1 -1
- package/dist/protocol/raw-routes.d.ts +0 -5
- package/dist/protocol/raw-routes.d.ts.map +1 -1
- package/dist/protocol/raw-routes.js +0 -1
- package/dist/protocol/raw-routes.js.map +1 -1
- package/dist/schemas/agents.d.ts +58 -0
- package/dist/schemas/agents.d.ts.map +1 -1
- package/dist/schemas/agents.js +9 -0
- package/dist/schemas/agents.js.map +1 -1
- package/dist/schemas/automations.d.ts +19 -0
- package/dist/schemas/automations.d.ts.map +1 -1
- package/dist/schemas/capabilities.d.ts +19 -0
- package/dist/schemas/capabilities.d.ts.map +1 -1
- package/dist/schemas/capabilities.js +16 -0
- package/dist/schemas/capabilities.js.map +1 -1
- package/dist/schemas/devices.d.ts +68 -0
- package/dist/schemas/devices.d.ts.map +1 -1
- package/dist/schemas/devices.js +7 -0
- package/dist/schemas/devices.js.map +1 -1
- package/dist/schemas/hosts.d.ts +2 -0
- package/dist/schemas/hosts.d.ts.map +1 -1
- package/dist/schemas/hosts.js +2 -1
- package/dist/schemas/hosts.js.map +1 -1
- package/dist/schemas/needs.d.ts +661 -0
- package/dist/schemas/needs.d.ts.map +1 -0
- package/dist/schemas/needs.js +196 -0
- package/dist/schemas/needs.js.map +1 -0
- package/dist/schemas/secrets.d.ts +18 -0
- package/dist/schemas/secrets.d.ts.map +1 -1
- package/dist/schemas/secrets.js +20 -0
- package/dist/schemas/secrets.js.map +1 -1
- package/dist/schemas/settings.d.ts +2 -0
- package/dist/schemas/settings.d.ts.map +1 -1
- package/dist/schemas/settings.js +4 -0
- package/dist/schemas/settings.js.map +1 -1
- package/dist/schemas/state-plan.d.ts +1 -0
- package/dist/schemas/state-plan.d.ts.map +1 -1
- package/dist/schemas/state-plan.js +3 -1
- package/dist/schemas/state-plan.js.map +1 -1
- package/dist/schemas/system.d.ts +20 -0
- package/dist/schemas/system.d.ts.map +1 -1
- package/dist/schemas/system.js +7 -0
- package/dist/schemas/system.js.map +1 -1
- package/dist/schemas/updates.d.ts +39 -0
- package/dist/schemas/updates.d.ts.map +1 -0
- package/dist/schemas/updates.js +31 -0
- package/dist/schemas/updates.js.map +1 -0
- package/dist/state/definition.d.ts +16 -0
- package/dist/state/definition.d.ts.map +1 -1
- package/dist/state/history-state.d.ts.map +1 -1
- package/dist/state/history-state.js +3 -0
- package/dist/state/history-state.js.map +1 -1
- package/dist/state/workspace-state.d.ts +4 -0
- package/dist/state/workspace-state.d.ts.map +1 -1
- package/dist/state/workspace-state.js +1 -0
- package/dist/state/workspace-state.js.map +1 -1
- package/dist/text/transcript-fold.d.ts.map +1 -1
- package/dist/text/transcript-fold.js +3 -0
- package/dist/text/transcript-fold.js.map +1 -1
- package/package.json +5 -5
- package/src/contracts/capabilities.contract.ts +14 -1
- package/src/contracts/needs.contract.ts +103 -0
- package/src/contracts/secrets.contract.ts +17 -3
- package/src/contracts/system.contract.ts +14 -0
- package/src/events/agent-events.ts +4 -0
- package/src/events/agent-words.ts +2 -1
- package/src/events/need-wake.test.ts +55 -0
- package/src/events/need-wake.ts +71 -0
- package/src/events/transcript.ts +16 -0
- package/src/index.ts +6 -0
- package/src/protocol/raw-routes.ts +1 -2
- package/src/schemas/agents.ts +14 -1
- package/src/schemas/capabilities.ts +28 -0
- package/src/schemas/devices.ts +17 -0
- package/src/schemas/hosts.ts +4 -1
- package/src/schemas/needs.ts +292 -0
- package/src/schemas/secrets.ts +23 -0
- package/src/schemas/settings.ts +8 -0
- package/src/schemas/state-plan.ts +8 -1
- package/src/schemas/system.ts +15 -0
- package/src/schemas/updates.ts +56 -0
- package/src/state/history-state.ts +5 -0
- package/src/state/workspace-state.ts +4 -0
- package/src/text/transcript-fold.ts +4 -0
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import { procedure } from "../protocol/route-meta.js";
|
|
2
|
+
import { OkSchema } from "../schemas/shared.js";
|
|
3
|
+
import {
|
|
4
|
+
GrantRevokeSchema,
|
|
5
|
+
NeedAnswerInputSchema,
|
|
6
|
+
NeedIdParamSchema,
|
|
7
|
+
NeedRaisedSchema,
|
|
8
|
+
NeedRaiseSchema,
|
|
9
|
+
NeedSchema,
|
|
10
|
+
NeedSecretInputSchema,
|
|
11
|
+
NeedsListSchema,
|
|
12
|
+
NeedsQuerySchema,
|
|
13
|
+
StandingGrantsSchema,
|
|
14
|
+
} from "../schemas/needs.js";
|
|
15
|
+
|
|
16
|
+
// Needs (docs/architecture/needs.md): the agent's CLIs raise and withdraw them on the agent token; a person reads and
|
|
17
|
+
// answers them from the chat, the board and the Needs you inbox. Answering is the operating tier's, like the capability
|
|
18
|
+
// and secret routes an answer ends up writing through.
|
|
19
|
+
|
|
20
|
+
const agentDoor = procedure.meta({ agent: true, control: "never" });
|
|
21
|
+
const answerRoute = procedure.meta({ floor: "maintainer", control: "never" });
|
|
22
|
+
|
|
23
|
+
export const needsContract = {
|
|
24
|
+
ask: agentDoor
|
|
25
|
+
.route({
|
|
26
|
+
method: "POST",
|
|
27
|
+
path: "/needs/ask",
|
|
28
|
+
summary: "Ask a person for something the task needs",
|
|
29
|
+
description:
|
|
30
|
+
"Raises a need in the conversation the calling shell belongs to and holds the call up to `wait` seconds for an answer. Answers `met` when it is usable now (or already was), `open` when it is still waiting, and `refused` when nothing was raised or a person declined. An open need's answer reaches the conversation by itself.",
|
|
31
|
+
})
|
|
32
|
+
// Held open while the person decides, by design.
|
|
33
|
+
.meta({ stream: true })
|
|
34
|
+
.input(NeedRaiseSchema)
|
|
35
|
+
.output(NeedRaisedSchema),
|
|
36
|
+
mine: agentDoor
|
|
37
|
+
.route({
|
|
38
|
+
method: "GET",
|
|
39
|
+
path: "/needs/mine",
|
|
40
|
+
summary: "This conversation's needs",
|
|
41
|
+
description: "Every need the calling shell's conversation raised, newest first, open or answered.",
|
|
42
|
+
})
|
|
43
|
+
.output(NeedsListSchema),
|
|
44
|
+
withdraw: agentDoor
|
|
45
|
+
.route({
|
|
46
|
+
method: "POST",
|
|
47
|
+
path: "/needs/{id}/withdraw",
|
|
48
|
+
summary: "Withdraw a need",
|
|
49
|
+
description: "Closes one of this conversation's open needs because the task no longer needs it. Its card says so.",
|
|
50
|
+
})
|
|
51
|
+
.input(NeedIdParamSchema)
|
|
52
|
+
.output(NeedSchema),
|
|
53
|
+
list: procedure
|
|
54
|
+
.route({
|
|
55
|
+
method: "GET",
|
|
56
|
+
path: "/needs",
|
|
57
|
+
summary: "What agents are waiting on people for",
|
|
58
|
+
description: "Needs across the sandbox, or one conversation's, newest first. Never a secret's value.",
|
|
59
|
+
})
|
|
60
|
+
.input(NeedsQuerySchema)
|
|
61
|
+
.output(NeedsListSchema),
|
|
62
|
+
answer: answerRoute
|
|
63
|
+
.route({
|
|
64
|
+
method: "POST",
|
|
65
|
+
path: "/needs/{id}/answer",
|
|
66
|
+
summary: "Answer a need",
|
|
67
|
+
description:
|
|
68
|
+
"Declines it, or says yes the way its card offered: accept a connection being set up, apply a change, grant for this conversation or the persona, release a gated credential, approve an environment proposal. A release is refused from anyone the gate does not name.",
|
|
69
|
+
})
|
|
70
|
+
.input(NeedAnswerInputSchema)
|
|
71
|
+
.output(NeedSchema),
|
|
72
|
+
provideSecret: answerRoute
|
|
73
|
+
.route({
|
|
74
|
+
method: "POST",
|
|
75
|
+
path: "/needs/{id}/secret",
|
|
76
|
+
summary: "Give a secret a need asked for",
|
|
77
|
+
description:
|
|
78
|
+
"Stores the value under the name the need asked for and meets it. The value goes to the sandbox's secret store and nowhere else: not the answer, not the transcript, not a log.",
|
|
79
|
+
})
|
|
80
|
+
// Puts a credential in motion: withheld from the panel token.
|
|
81
|
+
.meta({ panel: false })
|
|
82
|
+
.input(NeedSecretInputSchema)
|
|
83
|
+
.output(NeedSchema),
|
|
84
|
+
grants: answerRoute
|
|
85
|
+
.route({
|
|
86
|
+
method: "GET",
|
|
87
|
+
path: "/needs/grants",
|
|
88
|
+
summary: "The yeses still standing",
|
|
89
|
+
description:
|
|
90
|
+
"What people allowed conversations beyond their persona or area, and the gated credentials released to them, by conversation. Names only, never a value.",
|
|
91
|
+
})
|
|
92
|
+
.output(StandingGrantsSchema),
|
|
93
|
+
revokeGrant: answerRoute
|
|
94
|
+
.route({
|
|
95
|
+
method: "POST",
|
|
96
|
+
path: "/needs/grants/revoke",
|
|
97
|
+
summary: "Take a yes back",
|
|
98
|
+
description:
|
|
99
|
+
"Takes back one grant or one release. The conversation's next turn runs without it; a turn already running keeps what it mounted.",
|
|
100
|
+
})
|
|
101
|
+
.input(GrantRevokeSchema)
|
|
102
|
+
.output(OkSchema),
|
|
103
|
+
};
|
|
@@ -7,6 +7,8 @@ import {
|
|
|
7
7
|
CredentialRequestSchema,
|
|
8
8
|
SecretInventorySchema,
|
|
9
9
|
SecretKeyParamSchema,
|
|
10
|
+
SecretGeneratedSchema,
|
|
11
|
+
SecretGenerateSchema,
|
|
10
12
|
SecretKeysSchema,
|
|
11
13
|
SecretRevealSchema,
|
|
12
14
|
SecretSetSchema,
|
|
@@ -16,8 +18,8 @@ import { OkSchema } from "../schemas/shared.js";
|
|
|
16
18
|
// Credentials are the operating tier's, reads included: maintainer is the highest revokable grant, and no token's.
|
|
17
19
|
const secretRoute = procedure.meta({ floor: "maintainer", control: "never" });
|
|
18
20
|
|
|
19
|
-
// User-supplied secrets, in the gitignored desired-state/.env
|
|
20
|
-
// every store (never values) and always answers
|
|
21
|
+
// User-supplied secrets, in the gitignored desired-state/.env once DevOps is active and the sandbox's own store before
|
|
22
|
+
// it; `apply` reloads them, no restart. `inventory` aggregates every store (never values) and always answers. `reveal` alone
|
|
21
23
|
// returns a value, owner-only, POST so the key avoids the URL.
|
|
22
24
|
export const secretsContract = {
|
|
23
25
|
set: secretRoute
|
|
@@ -26,10 +28,22 @@ export const secretsContract = {
|
|
|
26
28
|
path: "/secrets",
|
|
27
29
|
summary: "Store a secret",
|
|
28
30
|
description:
|
|
29
|
-
"Writes one name and value
|
|
31
|
+
"Writes one name and value where the agent's references resolve it, without a restart: desired-state/.env once DevOps is active, the sandbox's own secret store before that.",
|
|
30
32
|
})
|
|
31
33
|
.input(SecretSetSchema)
|
|
32
34
|
.output(OkSchema),
|
|
35
|
+
generate: secretRoute
|
|
36
|
+
.route({
|
|
37
|
+
method: "POST",
|
|
38
|
+
path: "/secrets/generate",
|
|
39
|
+
summary: "Make and store a random secret",
|
|
40
|
+
description:
|
|
41
|
+
"Makes a random value and stores it under a new name, where `set` would have put it, for a secret nobody has to find or paste (a session key, a signing secret, a password the task sets up itself). Answers the name and its length, never the value. Refused for a name something here already holds.",
|
|
42
|
+
})
|
|
43
|
+
// The `secrets generate` CLI, on the agent token: it creates a value, and hands none back.
|
|
44
|
+
.meta({ agent: true })
|
|
45
|
+
.input(SecretGenerateSchema)
|
|
46
|
+
.output(SecretGeneratedSchema),
|
|
33
47
|
list: secretRoute
|
|
34
48
|
.route({
|
|
35
49
|
method: "GET",
|
|
@@ -15,6 +15,7 @@ import { PresenceReportSchema } from "../schemas/logs.js";
|
|
|
15
15
|
import { SandboxMetricsSchema, StorageCleanInputSchema, StorageCleanResultSchema, StorageReportSchema } from "../schemas/metrics.js";
|
|
16
16
|
import { OkSchema } from "../schemas/shared.js";
|
|
17
17
|
import { DaemonSessionSchema, InfoSchema, ManifestProblemsSchema, ManifestRepairSchema } from "../schemas/system.js";
|
|
18
|
+
import { SkipUpdateInputSchema } from "../schemas/updates.js";
|
|
18
19
|
import {
|
|
19
20
|
BrowserNameParamSchema,
|
|
20
21
|
BrowsersListSchema,
|
|
@@ -44,6 +45,19 @@ export const systemContract = {
|
|
|
44
45
|
})
|
|
45
46
|
.meta({ guest: true })
|
|
46
47
|
.output(InfoSchema),
|
|
48
|
+
// The owner's "not this one" for the update card; /info's `skippedVersion` reads it back.
|
|
49
|
+
skipUpdate: systemRoute
|
|
50
|
+
.route({
|
|
51
|
+
method: "POST",
|
|
52
|
+
path: "/system/update/skip",
|
|
53
|
+
summary: "Stop offering one release",
|
|
54
|
+
description:
|
|
55
|
+
"Stops offering the named release as an update, typically one this sandbox already tried and went back from. A newer release is offered as usual. Null offers the newest release again.",
|
|
56
|
+
})
|
|
57
|
+
// Which version the sandbox runs is the operator's decision, like the update itself.
|
|
58
|
+
.meta({ floor: "maintainer" })
|
|
59
|
+
.input(SkipUpdateInputSchema)
|
|
60
|
+
.output(OkSchema),
|
|
47
61
|
// Own route, not a field on /info: it goes stale on a manifest changing on disk, not on identity changing.
|
|
48
62
|
manifestProblems: systemRoute
|
|
49
63
|
.route({
|
|
@@ -5,6 +5,7 @@ import { LandConflictSchema } from "../schemas/agents.js";
|
|
|
5
5
|
import { ContextTrimSchema } from "../schemas/context-trim.js";
|
|
6
6
|
import { RateLimitInfoSchema } from "../schemas/providers/claude-gate.js";
|
|
7
7
|
import { FastModeStateSchema } from "../schemas/providers/fast-mode.js";
|
|
8
|
+
import { NeedSchema } from "../schemas/needs.js";
|
|
8
9
|
import { PromptCacheOpeningSchema, PromptFingerprintSchema } from "../schemas/keep-warm.js";
|
|
9
10
|
import { AgentReplySchema, UsageWindowSchema } from "../schemas/providers/plan-limits.js";
|
|
10
11
|
import { SubagentKindSchema, SubagentStatusSchema, SubagentVerificationSchema } from "../schemas/terminal.js";
|
|
@@ -206,6 +207,9 @@ export const AgentEventSchema = z.discriminatedUnion("kind", [
|
|
|
206
207
|
// Who released it: `released` names the approver's verified address, `refused` means someone said no. Nothing is
|
|
207
208
|
// pushed for a card nobody answered; `resolved` already says that.
|
|
208
209
|
CredentialReceiptSchema.extend({ kind: z.literal("credential_receipt"), requestId: z.string() }),
|
|
210
|
+
// Something the agent asked a person for (docs/architecture/needs.md). Parks nothing: the turn carries on, the need
|
|
211
|
+
// outlives it in the needs store, and its card draws the store's live state by `need.id`.
|
|
212
|
+
z.object({ kind: z.literal("need"), need: NeedSchema }),
|
|
209
213
|
// The named card is released and the turn resumes; emitted the moment its waiter settles, since nothing else on
|
|
210
214
|
// this stream marks a park's end. `reply` is what a rebuilt transcript freezes the card with.
|
|
211
215
|
z.object({ kind: z.literal("resolved"), requestId: z.string(), reply: AgentReplySchema.optional() }),
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { SubagentVerification } from "../schemas/terminal.js";
|
|
2
2
|
import type { TranscriptAgentWords, TranscriptRow } from "./transcript.js";
|
|
3
|
+
import { needWakeRow } from "./need-wake.js";
|
|
3
4
|
import { watchWakeRow } from "./watch-wake.js";
|
|
4
5
|
|
|
5
6
|
// Composer and parser of another agent's prompt (a peer's message, a child's report) are one piece of knowledge.
|
|
@@ -124,4 +125,4 @@ export const agentWordsRow = (prompt: string): TranscriptRow | undefined => {
|
|
|
124
125
|
};
|
|
125
126
|
|
|
126
127
|
/** The row for a prompt nobody at the composer typed; every transcript reader asks this one function. */
|
|
127
|
-
export const unspokenPromptRow = (prompt: string): TranscriptRow | undefined => watchWakeRow(prompt) ?? agentWordsRow(prompt);
|
|
128
|
+
export const unspokenPromptRow = (prompt: string): TranscriptRow | undefined => watchWakeRow(prompt) ?? needWakeRow(prompt) ?? agentWordsRow(prompt);
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { unspokenPromptRow } from "./agent-words.js";
|
|
2
|
+
import { needRowText, needWakeOf, needWakePrompt, needWakeRow, type NeedWakeFields, type NeedWakeOutcome } from "./need-wake.js";
|
|
3
|
+
|
|
4
|
+
const fields = (over: Partial<NeedWakeFields> = {}): NeedWakeFields => ({
|
|
5
|
+
outcome: "met",
|
|
6
|
+
id: "need-7k2q",
|
|
7
|
+
title: "Connect GitHub",
|
|
8
|
+
why: "to open the pull request for the fix",
|
|
9
|
+
result: 'GitHub is connected as "github".',
|
|
10
|
+
use: ["In this turn, prefix a command with GH_TOKEN={{secret:github/token}}."],
|
|
11
|
+
...over,
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
const OUTCOMES: NeedWakeOutcome[] = ["met", "declined"];
|
|
15
|
+
|
|
16
|
+
describe("need wake", () => {
|
|
17
|
+
// The composer and the parser are the same piece of knowledge; this holds them together when either is reworded.
|
|
18
|
+
it.each(OUTCOMES)("round-trips a %s wake back to its fields", (outcome) => {
|
|
19
|
+
const prompt = needWakePrompt(fields({ outcome }));
|
|
20
|
+
expect(needWakeOf(prompt)).toEqual({ outcome, title: "Connect GitHub", id: "need-7k2q", sent: prompt });
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
it("tells a met need to carry on and a declined one not to wait", () => {
|
|
24
|
+
expect(needWakePrompt(fields()).split("\n").at(-1)).toBe("Continue the task that needed it.");
|
|
25
|
+
expect(needWakePrompt(fields({ outcome: "declined", use: [] }))).not.toContain("Continue the task");
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
it("leaves the agent's reason out when it gave none, rather than printing an empty label", () => {
|
|
29
|
+
expect(needWakePrompt(fields({ why: undefined }))).not.toContain("You asked because:");
|
|
30
|
+
expect(needWakePrompt(fields())).toContain("You asked because: to open the pull request for the fix");
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
it("is not a wake once its labelled lines are gone", () => {
|
|
34
|
+
const prompt = needWakePrompt(fields());
|
|
35
|
+
expect(needWakeOf(prompt.replace("Need id: need-7k2q\n", ""))).toBeUndefined();
|
|
36
|
+
expect(needWakeOf(prompt.replace("Need: Connect GitHub\n", ""))).toBeUndefined();
|
|
37
|
+
expect(needWakeOf("please connect github")).toBeUndefined();
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
it("becomes a notice row that says what happened, and every transcript reader finds it", () => {
|
|
41
|
+
const prompt = needWakePrompt(fields());
|
|
42
|
+
const row = needWakeRow(prompt);
|
|
43
|
+
expect(row).toEqual({
|
|
44
|
+
role: "notice",
|
|
45
|
+
text: "Connect GitHub: done, and the agent carries on.",
|
|
46
|
+
needWake: { outcome: "met", title: "Connect GitHub", id: "need-7k2q", sent: prompt },
|
|
47
|
+
});
|
|
48
|
+
expect(unspokenPromptRow(prompt)).toEqual(row);
|
|
49
|
+
expect(needWakeRow(needWakePrompt(fields({ outcome: "declined" })))?.text).toBe("Connect GitHub: declined, and the agent was told.");
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
it("words a raised need's row for a reader that draws no card", () => {
|
|
53
|
+
expect(needRowText({ title: "The OPENAI_API_KEY secret" })).toBe("Needs a person: The OPENAI_API_KEY secret");
|
|
54
|
+
});
|
|
55
|
+
});
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import type { TranscriptNeedWake, TranscriptRow } from "./transcript.js";
|
|
2
|
+
|
|
3
|
+
// What a need says when its answer reaches a conversation, and how that reads to a person. Delivered as an ordinary
|
|
4
|
+
// turn prompt (a steer into a live turn, or a turn of its own), so composing it and recognising it stay one piece of
|
|
5
|
+
// knowledge, exactly as a condition watch's wake does (watch-wake.ts): the daemon's record, a steered live turn and a
|
|
6
|
+
// provider's session store all turn this prompt back into the same row.
|
|
7
|
+
|
|
8
|
+
export type NeedWakeOutcome = TranscriptNeedWake["outcome"];
|
|
9
|
+
|
|
10
|
+
// Per-outcome opening sentence, which is also what the parser anchors on: keep each unique and stable across releases,
|
|
11
|
+
// or a reworded opening un-recognises wakes already in a record and they read as the user's own words.
|
|
12
|
+
const OPENINGS: Record<NeedWakeOutcome, string> = {
|
|
13
|
+
met: "Need met: a person gave you what you asked for.",
|
|
14
|
+
declined: "Need declined: a person said no to what you asked for. Carry on without it, and say plainly what it would have enabled.",
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
// Labels the prompt writes and the parser reads back. `Need` and `Need id` are load-bearing on both sides.
|
|
18
|
+
const NEED = "Need: ";
|
|
19
|
+
const NEED_ID = "Need id: ";
|
|
20
|
+
const ASKED = "You asked because: ";
|
|
21
|
+
|
|
22
|
+
export interface NeedWakeFields {
|
|
23
|
+
readonly outcome: NeedWakeOutcome;
|
|
24
|
+
readonly id: string;
|
|
25
|
+
readonly title: string;
|
|
26
|
+
readonly why: string | undefined;
|
|
27
|
+
// The daemon's sentence on how it ended ("Connected as "github".", "Stored under OPENAI_API_KEY.").
|
|
28
|
+
readonly result: string;
|
|
29
|
+
// What the agent can do with it now, and what arrives on its next turn; empty for a decline.
|
|
30
|
+
readonly use: readonly string[];
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export const needWakePrompt = (fields: NeedWakeFields): string =>
|
|
34
|
+
[
|
|
35
|
+
OPENINGS[fields.outcome],
|
|
36
|
+
`${NEED}${fields.title}`,
|
|
37
|
+
`${NEED_ID}${fields.id}`,
|
|
38
|
+
...(fields.why === undefined || fields.why === "" ? [] : [`${ASKED}${fields.why}`]),
|
|
39
|
+
fields.result,
|
|
40
|
+
...fields.use,
|
|
41
|
+
...(fields.outcome === "met" ? ["Continue the task that needed it."] : []),
|
|
42
|
+
].join("\n");
|
|
43
|
+
|
|
44
|
+
const headline = (outcome: NeedWakeOutcome, title: string): string =>
|
|
45
|
+
outcome === "met" ? `${title}: done, and the agent carries on.` : `${title}: declined, and the agent was told.`;
|
|
46
|
+
|
|
47
|
+
const valueOn = (lines: readonly string[], label: string): string | undefined => lines.find((line) => line.startsWith(label))?.slice(label.length);
|
|
48
|
+
|
|
49
|
+
// The words a raised need's row carries for a reader that draws no card (search, an export, an older client); the card
|
|
50
|
+
// itself is drawn from the need.
|
|
51
|
+
export const needRowText = (need: { readonly title: string }): string => `Needs a person: ${need.title}`;
|
|
52
|
+
|
|
53
|
+
// Which need wake a stored prompt is, if any; undefined for every prompt that isn't one, so any reader can ask without
|
|
54
|
+
// checking first. One that opens as a wake but lost its labelled lines is not one.
|
|
55
|
+
export const needWakeOf = (prompt: string): TranscriptNeedWake | undefined => {
|
|
56
|
+
const outcome = (Object.keys(OPENINGS) as NeedWakeOutcome[]).find((key) => prompt.startsWith(OPENINGS[key]));
|
|
57
|
+
if (outcome === undefined) {
|
|
58
|
+
return undefined;
|
|
59
|
+
}
|
|
60
|
+
const lines = prompt.split("\n");
|
|
61
|
+
const title = valueOn(lines, NEED);
|
|
62
|
+
const id = valueOn(lines, NEED_ID);
|
|
63
|
+
return title === undefined || id === undefined ? undefined : { outcome, title, id, sent: prompt };
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
// The row a wake becomes: a notice, since a need being answered happened to the conversation rather than being said by
|
|
67
|
+
// either side. Undefined when the prompt is not a need wake at all.
|
|
68
|
+
export const needWakeRow = (prompt: string): TranscriptRow | undefined => {
|
|
69
|
+
const wake = needWakeOf(prompt);
|
|
70
|
+
return wake === undefined ? undefined : { role: "notice", text: headline(wake.outcome, wake.title), needWake: wake };
|
|
71
|
+
};
|
package/src/events/transcript.ts
CHANGED
|
@@ -3,6 +3,7 @@ import { AgentHarnessSchema, AgentProviderSchema } from "../schemas/agent.js";
|
|
|
3
3
|
import { ShareDetailSchema } from "../schemas/share.js";
|
|
4
4
|
import { TurnErrandSchema, TurnSpeakerSchema } from "../schemas/speaker.js";
|
|
5
5
|
import { SubagentKindSchema, SubagentStatusSchema, SubagentVerificationSchema } from "../schemas/terminal.js";
|
|
6
|
+
import { NeedSchema } from "../schemas/needs.js";
|
|
6
7
|
import { RetryLadderSchema } from "../schemas/turn-break.js";
|
|
7
8
|
import type { ToolCallContent, ToolCallLocation, ToolCallStatus, ToolKind} from "./requests.js";
|
|
8
9
|
import { browserHelpRequest, capabilityOfferRequest, CapabilityOutcomeSchema, credentialOfferRequest, CredentialReceiptSchema, paymentOfferRequest, PaymentReceiptSchema, PermissionAskSchema, permissionRequest, planRequest, questionRequest, terminalHelpRequest, TodoItemSchema, ToolCallContentSchema, ToolCallLocationSchema, ToolCallStatusSchema, ToolKindSchema } from "./requests.js";
|
|
@@ -155,6 +156,15 @@ export const TranscriptWatchWakeSchema = z.object({
|
|
|
155
156
|
});
|
|
156
157
|
export type TranscriptWatchWake = z.infer<typeof TranscriptWatchWakeSchema>;
|
|
157
158
|
|
|
159
|
+
// A need's answer that reached this conversation as a prompt (need-wake.ts); the row keeps the prompt and names the need.
|
|
160
|
+
export const TranscriptNeedWakeSchema = z.object({
|
|
161
|
+
outcome: z.enum(["met", "declined"]).describe("How the need ended: a person gave it, or said no."),
|
|
162
|
+
title: z.string().describe("The need, as its card leads with it."),
|
|
163
|
+
id: z.string().describe("The need's handle, which its live card is keyed by."),
|
|
164
|
+
sent: z.string().describe("The whole prompt the model received, disclosed under the row."),
|
|
165
|
+
});
|
|
166
|
+
export type TranscriptNeedWake = z.infer<typeof TranscriptNeedWakeSchema>;
|
|
167
|
+
|
|
158
168
|
// Another agent's words that reached this conversation as a prompt; the row keeps the prompt and names the sender.
|
|
159
169
|
export const TranscriptAgentWordsSchema = z.object({
|
|
160
170
|
kind: z.enum(["peer", "child"]).describe("Who sent it: another conversation in the workspace, or a subagent this one started."),
|
|
@@ -299,6 +309,12 @@ export const TranscriptRowSchema = z.object({
|
|
|
299
309
|
capabilityOffer: TranscriptCapabilityOfferSchema.optional().describe("The capability setup this row asked for, the decision, and the outcome."),
|
|
300
310
|
paymentOffer: TranscriptPaymentOfferSchema.optional().describe("The payment this row asked for, the decision, and the receipt."),
|
|
301
311
|
watchWake: TranscriptWatchWakeSchema.optional().describe("The condition watch that woke this conversation, and the prompt it was woken with."),
|
|
312
|
+
// A need is not a parked card: it holds no turn open, outlives the one that raised it, and its live state is the
|
|
313
|
+
// needs store's, keyed by `need.id`. What rides here is the need as it was raised, for a reader with no store.
|
|
314
|
+
need: NeedSchema.optional().describe(
|
|
315
|
+
"Something the agent asked a person for, as it was when raised. Its live state (answered, met) is read by its id, since it outlives the turn.",
|
|
316
|
+
),
|
|
317
|
+
needWake: TranscriptNeedWakeSchema.optional().describe("The answered need that reached this conversation, and the prompt it came as."),
|
|
302
318
|
agentWords: TranscriptAgentWordsSchema.optional().describe("Another agent's words that reached this conversation, whose they are, and the prompt they came as."),
|
|
303
319
|
backgroundJob: TranscriptBackgroundJobSchema.optional().describe("The background job this row marks the start of."),
|
|
304
320
|
credentialOffer: TranscriptCredentialOfferSchema.optional().describe(
|
package/src/index.ts
CHANGED
|
@@ -32,6 +32,7 @@ import { netdiskContract } from "./contracts/netdisk.contract.js";
|
|
|
32
32
|
import { providersContract } from "./contracts/providers.contract.js";
|
|
33
33
|
import { pushContract } from "./contracts/push.contract.js";
|
|
34
34
|
import { safetyContract } from "./contracts/safety.contract.js";
|
|
35
|
+
import { needsContract } from "./contracts/needs.contract.js";
|
|
35
36
|
import { secretsContract } from "./contracts/secrets.contract.js";
|
|
36
37
|
import { sessionsContract } from "./contracts/sessions.contract.js";
|
|
37
38
|
import { settingsContract } from "./contracts/settings.contract.js";
|
|
@@ -92,6 +93,7 @@ export { publicContract } from "./contracts/public.contract.js";
|
|
|
92
93
|
export { providersContract, type RunnableProviders, RunnableProvidersSchema } from "./contracts/providers.contract.js";
|
|
93
94
|
export { pushContract } from "./contracts/push.contract.js";
|
|
94
95
|
export { safetyContract } from "./contracts/safety.contract.js";
|
|
96
|
+
export { needsContract } from "./contracts/needs.contract.js";
|
|
95
97
|
export { secretsContract } from "./contracts/secrets.contract.js";
|
|
96
98
|
export { sessionsContract } from "./contracts/sessions.contract.js";
|
|
97
99
|
export { settingsContract } from "./contracts/settings.contract.js";
|
|
@@ -115,6 +117,7 @@ export * from "./events/land-breakage.js";
|
|
|
115
117
|
export * from "./events/land-conflict.js";
|
|
116
118
|
export * from "./events/verify-nudge.js";
|
|
117
119
|
export * from "./events/errands.js";
|
|
120
|
+
export * from "./events/need-wake.js";
|
|
118
121
|
export * from "./events/watch-wake.js";
|
|
119
122
|
export * from "./policy/request-status.js";
|
|
120
123
|
export * from "./events/child-run.js";
|
|
@@ -214,6 +217,7 @@ export * from "./schemas/providers/provider-subscriptions.js";
|
|
|
214
217
|
export * from "./schemas/public.js";
|
|
215
218
|
export * from "./schemas/push.js";
|
|
216
219
|
export * from "./schemas/git/remote-refs.js";
|
|
220
|
+
export * from "./schemas/needs.js";
|
|
217
221
|
export * from "./schemas/secrets.js";
|
|
218
222
|
export * from "./schemas/netdisk.js";
|
|
219
223
|
export * from "./schemas/sessions.js";
|
|
@@ -224,6 +228,7 @@ export * from "./schemas/shared.js";
|
|
|
224
228
|
export * from "./schemas/system-prompt.js";
|
|
225
229
|
export * from "./schemas/system.js";
|
|
226
230
|
export * from "./schemas/state-plan.js";
|
|
231
|
+
export * from "./schemas/updates.js";
|
|
227
232
|
export * from "./schemas/terminal.js";
|
|
228
233
|
export * from "./schemas/turn-break.js";
|
|
229
234
|
export * from "./schemas/keep-warm.js";
|
|
@@ -285,6 +290,7 @@ export const sandboxContract = {
|
|
|
285
290
|
public: publicContract,
|
|
286
291
|
providers: providersContract,
|
|
287
292
|
push: pushContract,
|
|
293
|
+
needs: needsContract,
|
|
288
294
|
secrets: secretsContract,
|
|
289
295
|
system: systemContract,
|
|
290
296
|
translator: translatorContract,
|
|
@@ -83,9 +83,8 @@ export const RAW_ROUTES = {
|
|
|
83
83
|
"GET /extensions/{id}/bundle": { lane: "bulk" },
|
|
84
84
|
// An extension backend's own namespace, proxied verbatim.
|
|
85
85
|
"ALL /x/*": {},
|
|
86
|
-
// The `capabilities` CLI
|
|
86
|
+
// The `capabilities` CLI's discovery by name; its ask is the needs door (contracts/needs.contract.ts).
|
|
87
87
|
"GET /capabilities/connectable": { floor: "maintainer", agent: true, control: "never" },
|
|
88
|
-
"POST /capabilities/ask": { floor: "maintainer", agent: true, control: "never" },
|
|
89
88
|
// The `sandboxes` CLI; every create parks on a card in the owner's chat first.
|
|
90
89
|
"GET /sandboxes": { agent: true },
|
|
91
90
|
"POST /sandboxes": { agent: true },
|
package/src/schemas/agents.ts
CHANGED
|
@@ -4,6 +4,7 @@ import { AgentHarnessSchema, AgentOriginSchema, AgentProviderSchema, Conversatio
|
|
|
4
4
|
import { LoopStateSchema } from "./loops.js";
|
|
5
5
|
import { LimitPolicySchema, RetryPolicySchema, TurnBreakPolicySchema, TurnBreakSchema } from "./turn-break.js";
|
|
6
6
|
import { KeepWarmSchema } from "./keep-warm.js";
|
|
7
|
+
import { AgentNeedSchema } from "./needs.js";
|
|
7
8
|
import { EMOJI_MAX_LENGTH, isSingleEmoji } from "../text/emoji.js";
|
|
8
9
|
// A fleet agent is any conversation with a registry entry, keyed by conversationId. Isolated ones own a git worktree
|
|
9
10
|
// (branch agent/<id>); workspace conversations have none, but both share one status/activity/cost lifecycle.
|
|
@@ -56,7 +57,7 @@ export const AgentAttentionSchema = z.object({
|
|
|
56
57
|
plan: z.boolean().describe("It has proposed a plan and is waiting for a yes."),
|
|
57
58
|
question: z.boolean().describe("It has asked you something."),
|
|
58
59
|
permission: z.boolean().describe("It wants to use a tool it needs permission for."),
|
|
59
|
-
// A missing capability (
|
|
60
|
+
// A missing capability on a card from before needs (docs/architecture/needs.md); lets the lane say "setup needed" rather than a generic
|
|
60
61
|
// pause.
|
|
61
62
|
capability: z.boolean().describe("It needs something connected that is not connected yet."),
|
|
62
63
|
// A gated credential parked on a named person's click; the one pause the board's reader may not be able to clear
|
|
@@ -65,6 +66,11 @@ export const AgentAttentionSchema = z.object({
|
|
|
65
66
|
.boolean()
|
|
66
67
|
.describe("It is waiting for a named person to release a credential. The one pause that may not be yours to clear, whatever your role."),
|
|
67
68
|
conflict: z.boolean().describe("Its work cannot be merged without somebody resolving a clash."),
|
|
69
|
+
// A need outlives its turn (docs/architecture/needs.md), so this can stand on a card that is not running at all.
|
|
70
|
+
need: z
|
|
71
|
+
.boolean()
|
|
72
|
+
.optional()
|
|
73
|
+
.describe("It asked a person for something it still needs: a connection, a secret, wider reach, a tool. Absent from a daemon older than needs."),
|
|
68
74
|
});
|
|
69
75
|
export type AgentAttention = z.infer<typeof AgentAttentionSchema>;
|
|
70
76
|
// What the last turn left open, measured at the moment it ended, unlike AgentAttentionSchema's live parked waits.
|
|
@@ -527,6 +533,13 @@ export const AgentSummarySchema = z.object({
|
|
|
527
533
|
.describe(
|
|
528
534
|
"Outside conditions this conversation is parked on, each of which will wake it. Absent means none, which is nearly every conversation: an armed watch is why a finished-looking agent starts working by itself, and why a hosted machine will not go idle.",
|
|
529
535
|
),
|
|
536
|
+
// Kept in the needs store, so unlike `jobs` this survives a restart and stands while the conversation is idle.
|
|
537
|
+
needs: z
|
|
538
|
+
.array(AgentNeedSchema)
|
|
539
|
+
.optional()
|
|
540
|
+
.describe(
|
|
541
|
+
"What it is waiting on people for and has not got yet, oldest first. Absent means nothing: an open need is why an idle-looking agent still needs you.",
|
|
542
|
+
),
|
|
530
543
|
// In memory only: a daemon restart leaves transcript rows naming jobs this list no longer carries.
|
|
531
544
|
jobs: z
|
|
532
545
|
.array(
|
|
@@ -104,6 +104,15 @@ export const SshConfigSchema = z.discriminatedUnion("auth", [
|
|
|
104
104
|
user: z.string().min(1).describe("Which user to connect as."),
|
|
105
105
|
privateKey: z.string().min(1).describe("The private key, whole. Stored with tight permissions and never echoed back."),
|
|
106
106
|
}),
|
|
107
|
+
// The sandbox made this key itself (capabilities.sshKey), so its private half never crossed the wire; stored and
|
|
108
|
+
// written exactly like a pasted one.
|
|
109
|
+
z.object({
|
|
110
|
+
auth: z.literal("generated").describe("Sign in with a key the sandbox generated. Its private half never left the sandbox."),
|
|
111
|
+
host: z.string().min(1).describe("The machine's address."),
|
|
112
|
+
port: z.coerce.number().default(22).describe("Which port it listens on."),
|
|
113
|
+
user: z.string().min(1).describe("Which user to connect as."),
|
|
114
|
+
privateKey: z.string().min(1).describe("The private half of the generated key. Stored with tight permissions and never echoed back."),
|
|
115
|
+
}),
|
|
107
116
|
z.object({
|
|
108
117
|
auth: z.literal("password").describe("Sign in with a password."),
|
|
109
118
|
host: z.string().min(1).describe("The machine's address."),
|
|
@@ -112,6 +121,14 @@ export const SshConfigSchema = z.discriminatedUnion("auth", [
|
|
|
112
121
|
password: z.string().min(1).describe("The password. Stored, never echoed back."),
|
|
113
122
|
}),
|
|
114
123
|
]);
|
|
124
|
+
// What a form sends where a generated key's private half goes: the token capabilities.sshKey answered with, which the
|
|
125
|
+
// add swaps for the key the daemon kept. VAULTED's sibling (policy/capability-secrets.ts) for a value not stored yet; a
|
|
126
|
+
// marker with nothing held behind it is refused, never written.
|
|
127
|
+
const STASHED_PREFIX = "__intentic_stashed__:";
|
|
128
|
+
export const stashedMarker = (token: string): string => `${STASHED_PREFIX}${token}`;
|
|
129
|
+
// The token a marker names; undefined for anything else (a real key, VAULTED, a blank).
|
|
130
|
+
export const stashedToken = (value: unknown): string | undefined =>
|
|
131
|
+
typeof value === "string" && value.startsWith(STASHED_PREFIX) && value.length > STASHED_PREFIX.length ? value.slice(STASHED_PREFIX.length) : undefined;
|
|
115
132
|
// What is optional about the in-sandbox Docker Engine: `gpu` is an IMAGE option (rides the Dockerfile overlay, needs a
|
|
116
133
|
// rebuild), everything else is an ENGINE option (rewrites daemon.json, restarts dockerd, no rebuild but stops running
|
|
117
134
|
// containers). Flat strings, not nested booleans, to match the manifest's own two-state convention.
|
|
@@ -460,3 +477,14 @@ export const CapabilityProbeSchema = z.object({
|
|
|
460
477
|
who: z.string().optional().describe("Who the service said the credential belongs to, when it said."),
|
|
461
478
|
});
|
|
462
479
|
export type CapabilityProbe = z.infer<typeof CapabilityProbeSchema>;
|
|
480
|
+
// POST /capabilities/ssh-key: a key pair made inside the sandbox for a connection being set up. The public half is the
|
|
481
|
+
// whole answer; the private half stays in the daemon until an add sends `stashedMarker(token)` where it goes.
|
|
482
|
+
export const SshKeySchema = z.object({
|
|
483
|
+
publicKey: z.string().describe("The public half, as the one line a server's authorized_keys holds: `ssh-ed25519 AAAA… intentic-<sandbox>`."),
|
|
484
|
+
token: z
|
|
485
|
+
.string()
|
|
486
|
+
.describe(
|
|
487
|
+
"Stands for the private half, which never leaves the sandbox. Sent wrapped as a marker in the private key's place, it installs that key once, and it lapses after thirty minutes.",
|
|
488
|
+
),
|
|
489
|
+
});
|
|
490
|
+
export type SshKey = z.infer<typeof SshKeySchema>;
|
package/src/schemas/devices.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
// What one of the user's own machines is running.
|
|
2
2
|
import { z } from "zod";
|
|
3
3
|
import { parseHostConnection, type DeviceFacts, DeviceFactsSchema, MachineIdSchema, userDistrosOf, WslEnvironmentSchema } from "./hosts.js";
|
|
4
|
+
import { RollbackTargetSchema, UpdateOutcomeSchema } from "./updates.js";
|
|
4
5
|
import { DEV_VERSION } from "../state/versions.js";
|
|
5
6
|
// Desktop-sync report shape shared by the agent, daemon and browser, produced only by the agent's own `deviceReport`.
|
|
6
7
|
// The agent never reports `sandboxes`; the docker half is filled in by whoever reads the report, scoped to the reader's
|
|
@@ -76,6 +77,19 @@ export const DeviceSandboxSchema = z.object({
|
|
|
76
77
|
resources: SandboxResourcesSchema.optional(),
|
|
77
78
|
// The update `ic sandbox prepare` built and left waiting for this sandbox; absent when nothing is staged.
|
|
78
79
|
staged: z.object({ image: z.string(), version: z.string().optional(), channel: z.string().optional() }).optional(),
|
|
80
|
+
// Everything below is read off an `ic` new enough to know it; an older ic's listing simply lacks it.
|
|
81
|
+
// What the running image says it is.
|
|
82
|
+
version: z.string().optional(),
|
|
83
|
+
// An interrupted swap left the old container set aside with no replacement: the sandbox is down, and `start` (or
|
|
84
|
+
// any swap) puts it back. `running` is false while this is true.
|
|
85
|
+
parked: z.boolean().optional(),
|
|
86
|
+
// A swap has just happened and the previous version is still parked and ready: until this moment the host watches
|
|
87
|
+
// the new one and goes back by itself if it keeps crashing, never becomes ready, or loses its tunnel.
|
|
88
|
+
probationUntil: z.number().optional(),
|
|
89
|
+
// What the host last did about this sandbox's version (the same record the sandbox reads as `lastUpdate`).
|
|
90
|
+
lastUpdate: UpdateOutcomeSchema.optional(),
|
|
91
|
+
// Newest first: what `rollback` would return to, then the older versions kept on this machine for `rollback --to`.
|
|
92
|
+
rollbackTargets: z.array(RollbackTargetSchema).optional(),
|
|
79
93
|
});
|
|
80
94
|
export type DeviceSandbox = z.infer<typeof DeviceSandboxSchema>;
|
|
81
95
|
// One operation on one sandbox, streamed as lines ending in a `result` or `error` frame. `prepare` builds the pending
|
|
@@ -118,6 +132,9 @@ export const DeviceSandboxFlowSchema = z.strictObject({
|
|
|
118
132
|
slug: z.string().min(1),
|
|
119
133
|
// Approved overlay's sha256, required only by `rebuild`; only content matching it is ever built.
|
|
120
134
|
hash: z.string().optional(),
|
|
135
|
+
// `rollback` only: a version or pinned image from the sandbox's `rollbackTargets` to go back to instead of the
|
|
136
|
+
// previous one. Sent only to an agent that advertises `rollback-to`: an older one refuses the field.
|
|
137
|
+
to: z.string().min(1).optional(),
|
|
121
138
|
// `set-shape` only, both required by it: the whole shape, and when it takes effect.
|
|
122
139
|
// Strict too, unlike the same shape read off a report (where a newer ic's extra field must not fail an older
|
|
123
140
|
// reader): an order carrying a field this agent does not know is refused rather than carried out without it.
|
package/src/schemas/hosts.ts
CHANGED
|
@@ -68,10 +68,13 @@ export type DeviceFacts = z.infer<typeof DeviceFactsSchema>;
|
|
|
68
68
|
// - `set-shape`: the `set-shape`/`forget-shape` ops, and `start`/`restart` applying a saved shape, all through an `ic`
|
|
69
69
|
// whose `ic sandbox shape` takes the contract's own shape (`--set`); advertised only when the agent's `ic` does.
|
|
70
70
|
// Both features ride that verb, so an agent advertises both or neither.
|
|
71
|
-
|
|
71
|
+
// - `rollback-to`: the `rollback` op's `to`, going back to an older version kept on the machine rather than the
|
|
72
|
+
// previous one; advertised only when the agent's `ic sandbox rollback` takes `--to`.
|
|
73
|
+
export const DeviceFeatureSchema = z.enum(["reshape-later", "set-shape", "rollback-to"]);
|
|
72
74
|
export type DeviceFeature = z.infer<typeof DeviceFeatureSchema>;
|
|
73
75
|
export const DEVICE_FEATURE_RESHAPE_LATER: DeviceFeature = "reshape-later";
|
|
74
76
|
export const DEVICE_FEATURE_SET_SHAPE: DeviceFeature = "set-shape";
|
|
77
|
+
export const DEVICE_FEATURE_ROLLBACK_TO: DeviceFeature = "rollback-to";
|
|
75
78
|
// The features an agent advertised that this build knows; an unknown one is a newer agent's and means nothing here.
|
|
76
79
|
export const deviceFeatures = (facts: Pick<DeviceFacts, "features"> | undefined): DeviceFeature[] =>
|
|
77
80
|
(facts?.features ?? []).flatMap((feature) => {
|