@intentic/sandbox-contract 1.243.0 → 1.244.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/README.md +4 -1
- package/dist/agent-catalog.d.ts +2 -1
- package/dist/agent-catalog.d.ts.map +1 -1
- package/dist/agent-catalog.js +3 -2
- package/dist/agent-catalog.js.map +1 -1
- package/dist/card-status.d.ts.map +1 -1
- package/dist/card-status.js +8 -1
- package/dist/card-status.js.map +1 -1
- package/dist/chores/stack.d.ts.map +1 -1
- package/dist/chores/stack.js +2 -2
- package/dist/chores/stack.js.map +1 -1
- package/dist/command-classes.d.ts +1 -0
- package/dist/command-classes.d.ts.map +1 -1
- package/dist/command-classes.js +2 -2
- package/dist/command-classes.js.map +1 -1
- package/dist/contracts/agent.contract.d.ts +106 -0
- package/dist/contracts/agent.contract.d.ts.map +1 -1
- package/dist/contracts/agents.contract.d.ts +50 -0
- package/dist/contracts/agents.contract.d.ts.map +1 -1
- package/dist/contracts/runner.contract.d.ts +138 -92
- package/dist/contracts/runner.contract.d.ts.map +1 -1
- package/dist/contracts/secrets.contract.d.ts +50 -0
- package/dist/contracts/secrets.contract.d.ts.map +1 -1
- package/dist/contracts/secrets.contract.js +36 -1
- package/dist/contracts/secrets.contract.js.map +1 -1
- package/dist/contracts/sessions.contract.d.ts +37 -0
- package/dist/contracts/sessions.contract.d.ts.map +1 -1
- package/dist/contracts/settings.contract.d.ts +12 -0
- package/dist/contracts/settings.contract.d.ts.map +1 -1
- package/dist/contracts/system.contract.d.ts +39 -0
- package/dist/contracts/system.contract.d.ts.map +1 -1
- package/dist/definition.d.ts +8 -0
- package/dist/definition.d.ts.map +1 -1
- package/dist/events.d.ts +440 -1
- package/dist/events.d.ts.map +1 -1
- package/dist/events.js +40 -4
- package/dist/events.js.map +1 -1
- package/dist/index.d.ts +294 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/safety-policy.d.ts +6 -0
- package/dist/safety-policy.d.ts.map +1 -1
- package/dist/safety-policy.js +1 -0
- package/dist/safety-policy.js.map +1 -1
- package/dist/schemas/agents.d.ts +4 -0
- package/dist/schemas/agents.d.ts.map +1 -1
- package/dist/schemas/agents.js +3 -0
- package/dist/schemas/agents.js.map +1 -1
- package/dist/schemas/automations.d.ts +1 -0
- package/dist/schemas/automations.d.ts.map +1 -1
- package/dist/schemas/engines.d.ts +2 -0
- package/dist/schemas/engines.d.ts.map +1 -1
- package/dist/schemas/engines.js +1 -0
- package/dist/schemas/engines.js.map +1 -1
- package/dist/schemas/plan-limits.d.ts +4 -0
- package/dist/schemas/plan-limits.d.ts.map +1 -1
- package/dist/schemas/plan-limits.js +7 -0
- package/dist/schemas/plan-limits.js.map +1 -1
- package/dist/schemas/secrets.d.ts +75 -0
- package/dist/schemas/secrets.d.ts.map +1 -1
- package/dist/schemas/secrets.js +44 -0
- package/dist/schemas/secrets.js.map +1 -1
- package/dist/schemas/settings.d.ts +6 -0
- package/dist/schemas/settings.d.ts.map +1 -1
- package/dist/schemas/settings.js +7 -0
- package/dist/schemas/settings.js.map +1 -1
- package/dist/transcript-fold.d.ts.map +1 -1
- package/dist/transcript-fold.js +11 -0
- package/dist/transcript-fold.js.map +1 -1
- package/package.json +4 -4
- package/src/agent-catalog.test.ts +21 -4
- package/src/agent-catalog.ts +37 -13
- package/src/card-status.ts +13 -1
- package/src/chores/stack.test.ts +36 -0
- package/src/chores/stack.ts +13 -2
- package/src/command-classes.ts +8 -3
- package/src/contracts/secrets.contract.ts +67 -1
- package/src/events.ts +100 -11
- package/src/safety-policy.ts +23 -0
- package/src/schemas/agents.ts +6 -0
- package/src/schemas/engines.ts +1 -0
- package/src/schemas/plan-limits.ts +19 -0
- package/src/schemas/secrets.ts +110 -0
- package/src/schemas/settings.ts +45 -11
- package/src/transcript-fold.test.ts +47 -0
- package/src/transcript-fold.ts +11 -0
package/src/events.ts
CHANGED
|
@@ -4,6 +4,7 @@ import { AgentSummarySchema, LandConflictSchema } from "./schemas/agents.js";
|
|
|
4
4
|
import { RateLimitInfoSchema } from "./schemas/claude-gate.js";
|
|
5
5
|
import { FastModeStateSchema } from "./schemas/fast-mode.js";
|
|
6
6
|
import { AccountUsageSchema, AgentReplySchema, ProviderRefusalSchema, UsageWindowSchema } from "./schemas/plan-limits.js";
|
|
7
|
+
import { CredentialGateKindSchema, CredentialGateScopeSchema, CredentialLaneSchema } from "./schemas/secrets.js";
|
|
7
8
|
import { ShareDetailSchema } from "./schemas/share.js";
|
|
8
9
|
import { MemberRoleSchema } from "./schemas/shared.js";
|
|
9
10
|
import { SubagentKindSchema, SubagentStatusSchema, SubagentVerificationSchema } from "./schemas/terminal.js";
|
|
@@ -35,10 +36,16 @@ export type AskQuestion = z.infer<typeof AskQuestionSchema>;
|
|
|
35
36
|
* card with no way to know it was holding four hundred characters of shell: it rendered them as a paragraph,
|
|
36
37
|
* wrapped mid-flag, and the fragment that caused the hold was somewhere in the middle of it.
|
|
37
38
|
*
|
|
38
|
-
* `spans` is the
|
|
39
|
+
* `spans` is where the pattern match fired, computed by the classifier at the moment it did (contract's
|
|
39
40
|
* command-classes.ts, matchCommand) and carried rather than re-derived: a browser that re-ran the patterns
|
|
40
|
-
* would be a second classifier, and the day the two disagreed the card would be marking a fragment
|
|
41
|
-
*
|
|
41
|
+
* would be a second classifier, and the day the two disagreed the card would be marking a fragment the daemon
|
|
42
|
+
* never saw. Offsets are into `text` AFTER truncation, so they are always paintable.
|
|
43
|
+
*
|
|
44
|
+
* IT IS NOT A CLAIM ABOUT WHY THE CARD EXISTS, and the card no longer presents it as one. The reason is the
|
|
45
|
+
* judge's sentence in the title; these are the fragments TRIAGE noticed, all of the matched classes' rather
|
|
46
|
+
* than whichever sorts first — the card used to show one class's and label them "Stopped for", so a command
|
|
47
|
+
* that cleaned a build directory on its way to publishing offered `rm -rf …` as its reason under a sentence
|
|
48
|
+
* about npm. Under the hard rule the title DOES name a class, so there the marks are that class's alone.
|
|
42
49
|
*
|
|
43
50
|
* `language` is a Shiki grammar id, and the two are the two execution backends the gate reads (command-gate's
|
|
44
51
|
* EXECUTION_SOURCES): a shell line and a script. */
|
|
@@ -52,7 +59,9 @@ export const ProgramAskSchema = z.object({
|
|
|
52
59
|
),
|
|
53
60
|
spans: z
|
|
54
61
|
.array(z.object({ start: z.number().int().nonnegative(), end: z.number().int().nonnegative() }))
|
|
55
|
-
.describe(
|
|
62
|
+
.describe(
|
|
63
|
+
"Which fragments of the text the pattern match fired on: every matched class's, or, under the hard rule, only the class the title names. Offsets into text, in order, never overlapping.",
|
|
64
|
+
),
|
|
56
65
|
});
|
|
57
66
|
export type ProgramAsk = z.infer<typeof ProgramAskSchema>;
|
|
58
67
|
|
|
@@ -80,16 +89,20 @@ export const PermissionAskSchema = z.object({
|
|
|
80
89
|
program: ProgramAskSchema.optional().describe(
|
|
81
90
|
"The program this card is holding, when the card is about one. Present on a command gate's card and absent on every other permission ask.",
|
|
82
91
|
),
|
|
83
|
-
/* THE JUDGE'S OWN SENTENCE,
|
|
84
|
-
*
|
|
85
|
-
*
|
|
86
|
-
*
|
|
87
|
-
*
|
|
92
|
+
/* THE JUDGE'S OWN SENTENCE, WHERE THE TITLE IS SOMEBODY ELSE'S. On an ordinary command card the sentence IS
|
|
93
|
+
* the title (the judge read the owner's policy and the program, and its account of why this needs asking is
|
|
94
|
+
* the only account there is), so this is left off rather than printing the same words twice. It carries the
|
|
95
|
+
* sentence on the two cards whose title says something the sentence cannot: the hard rule's, which names the
|
|
96
|
+
* consequence that stopped it, and a machine command's, which names the computer.
|
|
97
|
+
*
|
|
98
|
+
* Written by the quick model from the program text and the policy, never by the agent being gated — a card
|
|
99
|
+
* whose persuasive half was authored by the thing it is stopping argues for its own approval, and the turns
|
|
100
|
+
* that raise cards are exactly the ones whose account of themselves may be a stranger's. */
|
|
88
101
|
explain: z
|
|
89
102
|
.string()
|
|
90
103
|
.optional()
|
|
91
104
|
.describe(
|
|
92
|
-
"One plain sentence saying what the program does and why it is being asked about. Written by the judge that read your safety policy, never by the agent being gated.",
|
|
105
|
+
"One plain sentence saying what the program does and why it is being asked about, where the title says something else. Written by the judge that read your safety policy, never by the agent being gated.",
|
|
93
106
|
),
|
|
94
107
|
});
|
|
95
108
|
export type PermissionAsk = z.infer<typeof PermissionAskSchema>;
|
|
@@ -209,6 +222,39 @@ export const PaymentOfferSchema = z.object({
|
|
|
209
222
|
});
|
|
210
223
|
export type PaymentOffer = z.infer<typeof PaymentOfferSchema>;
|
|
211
224
|
|
|
225
|
+
/* ONE GATED CREDENTIAL, ASKED FOR, the card the daemon raises when the agent reaches for a secret or a
|
|
226
|
+
* connected account the owner put behind a named person (secrets/credential-gate.ts).
|
|
227
|
+
*
|
|
228
|
+
* Every field but `why` is the daemon's own: the subject and its approvers come off the gate policy the owner
|
|
229
|
+
* wrote (which lives off the workspace, where the agent cannot edit it), the lane and detail come from the
|
|
230
|
+
* exit that was about to spend the credential, and the scope is the policy's, not the asker's. The model
|
|
231
|
+
* contributes one line of rationale and nothing else, which is what makes the card impossible to
|
|
232
|
+
* misrepresent: a prompt-injected turn can ask for the production password and cannot make the card say it is
|
|
233
|
+
* asking for the staging one.
|
|
234
|
+
*
|
|
235
|
+
* THE APPROVERS ARE ON THE CARD because the card is not addressed to "the owner" the way every other offer
|
|
236
|
+
* here is — it is addressed to a LIST, the server checks the clicker's verified identity against it, and a
|
|
237
|
+
* click from anybody else is refused with the card left standing. So the names have to be visible: a card
|
|
238
|
+
* whose buttons do nothing for the person looking at it must say who it is waiting for. */
|
|
239
|
+
export const CredentialOfferSchema = z.object({
|
|
240
|
+
// The gate's subject: a secret's reference name (`DATABASE_URL`) or a capability id (`reddit`).
|
|
241
|
+
subject: z.string().describe("Which credential is being asked for."),
|
|
242
|
+
kind: CredentialGateKindSchema,
|
|
243
|
+
lane: CredentialLaneSchema,
|
|
244
|
+
// Where it would go, in the reader's terms: the head of the agent's command line, the page's host, or the
|
|
245
|
+
// capability's own name. Reference-form by construction on the secret lanes (resolution is what fires the
|
|
246
|
+
// ask), so this can be shown without leaking anything.
|
|
247
|
+
detail: z
|
|
248
|
+
.string()
|
|
249
|
+
.optional()
|
|
250
|
+
.describe("Where it would go: the start of the command, the site, or what is being mounted. Never a value: the command still reads as a reference at this point."),
|
|
251
|
+
// The agent's one-line case, the only prose on the card that is the model's.
|
|
252
|
+
why: z.string().optional().describe("The agent's case for using it, and the only words on this card that are the agent's."),
|
|
253
|
+
approvers: z.array(z.string()).describe("Who may release it. A click from anyone else is refused and leaves the card standing."),
|
|
254
|
+
scope: CredentialGateScopeSchema,
|
|
255
|
+
});
|
|
256
|
+
export type CredentialOffer = z.infer<typeof CredentialOfferSchema>;
|
|
257
|
+
|
|
212
258
|
// One provider-advertised slash command, an ACP agent's available_commands entry, or a Claude Code session's
|
|
213
259
|
// supportedCommands() (its built-ins plus the workspace's own .claude/commands and any plugin/skill commands).
|
|
214
260
|
// `hint` is the argument placeholder the popover shows after the name.
|
|
@@ -360,6 +406,7 @@ const terminalHelpCard = {
|
|
|
360
406
|
const serviceOfferCard = { requestId: z.string(), offer: ServiceOfferSchema };
|
|
361
407
|
const capabilityOfferCard = { requestId: z.string(), offer: CapabilityOfferSchema };
|
|
362
408
|
const paymentOfferCard = { requestId: z.string(), offer: PaymentOfferSchema };
|
|
409
|
+
const credentialOfferCard = { requestId: z.string(), offer: CredentialOfferSchema };
|
|
363
410
|
|
|
364
411
|
/* HOW AN OFFER'S ACCEPTED HALF ENDED, the follow-up that lands on the card after the click. Each is the body of
|
|
365
412
|
* the frame that reports it (`service_receipt`, `capability_outcome`, `payment_receipt`) and the field the
|
|
@@ -382,6 +429,15 @@ export const PaymentReceiptSchema = z.object({
|
|
|
382
429
|
network: z.string().optional(),
|
|
383
430
|
});
|
|
384
431
|
export type PaymentReceipt = z.infer<typeof PaymentReceiptSchema>;
|
|
432
|
+
/* WHO RELEASED A GATED CREDENTIAL, or that a person refused it. `released` carries the approver's own address,
|
|
433
|
+
* read off the VERIFIED identity on the reply rather than off anything the click claimed, which is what makes
|
|
434
|
+
* the row an audit line rather than a rendering. There is no receipt for a card nobody answered: `resolved`
|
|
435
|
+
* already says so, and inventing "refused" for a deadline would put words in a person's mouth. */
|
|
436
|
+
export const CredentialReceiptSchema = z.object({
|
|
437
|
+
outcome: z.enum(["released", "refused"]),
|
|
438
|
+
approvedBy: z.string().optional(),
|
|
439
|
+
});
|
|
440
|
+
export type CredentialReceipt = z.infer<typeof CredentialReceiptSchema>;
|
|
385
441
|
|
|
386
442
|
/* THE THREE RESTORABLE CARDS, named so the turn journal can hold them verbatim: a parked turn's raised cards
|
|
387
443
|
* are written down beside its prompt (sandbox turn-journal.ts), and a daemon death under the park restores the
|
|
@@ -468,6 +524,12 @@ export const TranscriptPaymentOfferSchema = z.object({
|
|
|
468
524
|
receipt: PaymentReceiptSchema.optional().describe("How the approved payment ended (the payment_receipt frame)."),
|
|
469
525
|
});
|
|
470
526
|
export type TranscriptPaymentOffer = z.infer<typeof TranscriptPaymentOfferSchema>;
|
|
527
|
+
export const TranscriptCredentialOfferSchema = z.object({
|
|
528
|
+
...credentialOfferCard,
|
|
529
|
+
status: OfferStatusSchema.describe("Where the decision stands."),
|
|
530
|
+
receipt: CredentialReceiptSchema.optional().describe("Who released it, or that somebody refused (the credential_receipt frame)."),
|
|
531
|
+
});
|
|
532
|
+
export type TranscriptCredentialOffer = z.infer<typeof TranscriptCredentialOfferSchema>;
|
|
471
533
|
|
|
472
534
|
// ---- transcript rows ----
|
|
473
535
|
// What a conversation is made of, on every surface: the rows the daemon folds a turn's frames into as they
|
|
@@ -664,13 +726,26 @@ export const TranscriptRowSchema = z.object({
|
|
|
664
726
|
serviceOffer: TranscriptServiceOfferSchema.optional().describe("The priced service run this row offered, the decision, and the receipt."),
|
|
665
727
|
capabilityOffer: TranscriptCapabilityOfferSchema.optional().describe("The capability setup this row asked for, the decision, and the outcome."),
|
|
666
728
|
paymentOffer: TranscriptPaymentOfferSchema.optional().describe("The payment this row asked for, the decision, and the receipt."),
|
|
729
|
+
credentialOffer: TranscriptCredentialOfferSchema.optional().describe(
|
|
730
|
+
"The gated credential this row asked to use, who may release it, and who did.",
|
|
731
|
+
),
|
|
667
732
|
});
|
|
668
733
|
export type TranscriptRow = z.infer<typeof TranscriptRowSchema>;
|
|
669
734
|
|
|
670
735
|
/* THE CARD FIELDS A ROW CAN CARRY, as one list, for every reader that has to ask "does this row hold a card":
|
|
671
736
|
* the fold that counts a card-only bubble as a row, the chat's row count (a branch is cut by it), and the
|
|
672
737
|
* surfaces that draw whichever card a bubble is waiting on. */
|
|
673
|
-
export const CARD_FIELDS = [
|
|
738
|
+
export const CARD_FIELDS = [
|
|
739
|
+
"plan",
|
|
740
|
+
"question",
|
|
741
|
+
"permission",
|
|
742
|
+
"browserHelp",
|
|
743
|
+
"terminalHelp",
|
|
744
|
+
"serviceOffer",
|
|
745
|
+
"capabilityOffer",
|
|
746
|
+
"paymentOffer",
|
|
747
|
+
"credentialOffer",
|
|
748
|
+
] as const;
|
|
674
749
|
export type CardField = (typeof CARD_FIELDS)[number];
|
|
675
750
|
export type TranscriptCards = Pick<TranscriptRow, CardField>;
|
|
676
751
|
// Whether a row holds a card at all, answered or not.
|
|
@@ -1164,6 +1239,20 @@ export const AgentEventSchema = z.discriminatedUnion("kind", [
|
|
|
1164
1239
|
* case the signed authorization expires unused and NOTHING left the wallet. A skip needs no receipt,
|
|
1165
1240
|
* nothing moved, and `resolved` already says so. */
|
|
1166
1241
|
PaymentReceiptSchema.extend({ kind: z.literal("payment_receipt"), requestId: z.string() }),
|
|
1242
|
+
/* A GATED CREDENTIAL awaiting a NAMED person's click, the one card on this stream that is not addressed to
|
|
1243
|
+
* the owner: the daemon holds an exit (a `{{secret:…}}` about to resolve, a browser field about to be
|
|
1244
|
+
* typed into, a connected account about to be mounted) parked until one of the gate's approvers releases
|
|
1245
|
+
* it (secrets/credential-gate.ts). Raised OUTSIDE the turn generator like the offers above — the exits run
|
|
1246
|
+
* inside a PreToolUse hook and inside the daemon's own `secrets request` route — so it is not journalled
|
|
1247
|
+
* for restore: its waiter is a held hook or a held connection, both of which die with the daemon, and the
|
|
1248
|
+
* next use after a restart simply asks again. Settles through the same `POST /agent/reply`, which is where
|
|
1249
|
+
* the clicker's identity is checked against `offer.approvers`. */
|
|
1250
|
+
z.object({ kind: z.literal("credential_offer"), ...credentialOfferCard }),
|
|
1251
|
+
/* WHO RELEASED IT, pushed the moment a person decided, so the settled card names them rather than saying
|
|
1252
|
+
* only that something was approved: `released` with the approver's verified address, or `refused` when a
|
|
1253
|
+
* person said no. Nothing is pushed for a card nobody answered — `resolved` already says that, and a
|
|
1254
|
+
* deadline is not a refusal by anybody. */
|
|
1255
|
+
CredentialReceiptSchema.extend({ kind: z.literal("credential_receipt"), requestId: z.string() }),
|
|
1167
1256
|
// The card above named by `requestId` is released, the user answered (or dismissed it, or the turn was
|
|
1168
1257
|
// stopped out from under it), so the turn is executing again. Emitted by whoever parked, the moment its
|
|
1169
1258
|
// waiter settles, because the park's END is otherwise invisible on this stream: nothing else here says
|
package/src/safety-policy.ts
CHANGED
|
@@ -46,6 +46,29 @@ import type { CommandClass } from "./schemas/agent.js";
|
|
|
46
46
|
* set shrinks to block devices when that lands. */
|
|
47
47
|
export const HARD_RULE_CLASSES: ReadonlySet<CommandClass> = new Set<CommandClass>(["system.destructive"]);
|
|
48
48
|
|
|
49
|
+
/* WHETHER THE JUDGE RUNS AT ALL, and whether its answer is allowed to stop anything. The owner's switch over
|
|
50
|
+
* everything below, and the reason it exists is that a tier which spends a model call and can interrupt you is a
|
|
51
|
+
* tier somebody is entitled to decline — the old rulebook could be set to allow everything, and losing that when
|
|
52
|
+
* the judge arrived made the redesign a thing you could only opt further INTO.
|
|
53
|
+
*
|
|
54
|
+
* THREE STATES, and the middle one is the one worth arguing for. Nobody trusts a judge they have not watched,
|
|
55
|
+
* and the only evidence that it asks about the right things is a log of what it decided while it could not
|
|
56
|
+
* interrupt them. Same shape, and the same reasoning, as the tier judge's own Measure state (settings.autoTier).
|
|
57
|
+
*
|
|
58
|
+
* off nothing is judged. No model call, no cards, nothing written to the log. Triage still runs, because
|
|
59
|
+
* the hard rule below is built on it and costs nothing.
|
|
60
|
+
* watch the judge runs on every triage hit and every verdict is recorded, and NOTHING is ever held: an `ask`
|
|
61
|
+
* is logged as an ask and the command runs anyway. What it costs is one model call per triage hit;
|
|
62
|
+
* what it buys is the Recent decisions list, read against a policy nobody has tested yet.
|
|
63
|
+
* on the verdict decides, which is the behaviour this design describes everywhere else.
|
|
64
|
+
*
|
|
65
|
+
* THE HARD RULE IS NOT UNDER THIS SWITCH, at any setting. HARD_RULE_CLASSES is a typed verdict rather than a
|
|
66
|
+
* judgment, it never needed a model, and the Safety page promises in as many words that it cannot be edited
|
|
67
|
+
* away. So `off` and `watch` still raise a card for wiping a block device or deleting under /history — with a
|
|
68
|
+
* sentence saying the judge did not weigh in, rather than one pretending it did. */
|
|
69
|
+
export const CommandJudgeModeSchema = z.enum(["off", "watch", "on"]);
|
|
70
|
+
export type CommandJudgeMode = z.infer<typeof CommandJudgeModeSchema>;
|
|
71
|
+
|
|
49
72
|
/* WHAT THE JUDGE ANSWERS. Three verdicts, and each is a different instruction to the gate:
|
|
50
73
|
*
|
|
51
74
|
* allow run it, say nothing, nobody is interrupted. The ordinary answer for a triage false positive,
|
package/src/schemas/agents.ts
CHANGED
|
@@ -86,6 +86,12 @@ export const AgentAttentionSchema = z.object({
|
|
|
86
86
|
// A missing capability parked on the owner's setup (capabilities/capability-offer.ts), the agent is
|
|
87
87
|
// waiting for something to be connected, so the lane can say "setup needed" rather than a generic pause.
|
|
88
88
|
capability: z.boolean().describe("It needs something connected that is not connected yet."),
|
|
89
|
+
// A gated credential parked on a NAMED person's click (secrets/credential-gate.ts). Its own lane because
|
|
90
|
+
// it is the one pause the person reading the board may not be able to clear: every other card here waits
|
|
91
|
+
// for whoever is looking at it, this one waits for the specific people the gate names.
|
|
92
|
+
credential: z
|
|
93
|
+
.boolean()
|
|
94
|
+
.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."),
|
|
89
95
|
conflict: z.boolean().describe("Its work cannot be merged without somebody resolving a clash."),
|
|
90
96
|
});
|
|
91
97
|
export type AgentAttention = z.infer<typeof AgentAttentionSchema>;
|
package/src/schemas/engines.ts
CHANGED
|
@@ -82,6 +82,7 @@ export const EngineRowSchema = z.object({
|
|
|
82
82
|
previous: z.string().optional().describe("The version kept one step back, which is what going back means."),
|
|
83
83
|
quarantined: z.array(EngineQuarantineSchema).describe("Versions the store installed and then refused, with the reason."),
|
|
84
84
|
diskBytes: z.number().int().nonnegative().describe("What this engine's kept versions cost on the daemon's volume."),
|
|
85
|
+
installing: z.boolean().optional().describe("Whether this engine is currently being installed in the background."),
|
|
85
86
|
});
|
|
86
87
|
export type EngineRow = z.infer<typeof EngineRowSchema>;
|
|
87
88
|
|
|
@@ -259,6 +259,25 @@ export const AgentReplySchema = z.discriminatedUnion("kind", [
|
|
|
259
259
|
.boolean()
|
|
260
260
|
.describe("Yes releases exactly one payment. Anything else spends nothing. This click is the only way the money can move."),
|
|
261
261
|
}),
|
|
262
|
+
/* A GATED CREDENTIAL's release, and the one reply on this list whose sender is CHECKED. Every other card
|
|
263
|
+
* here is answered by whoever holds a session, because every other card is the owner's to decide; this one
|
|
264
|
+
* is addressed to a named list, so the daemon reads the verified identity off the request and refuses a
|
|
265
|
+
* click from anybody else with the card left standing for whoever can answer it. The refusal covers `no`
|
|
266
|
+
* as well: a stranger may not skip a release on the approver's behalf either, or the gate would be a
|
|
267
|
+
* denial-of-service anybody with a session could aim at a turn.
|
|
268
|
+
*
|
|
269
|
+
* `approve` carries no qualifiers because the gate's own policy decides how far one yes goes: a per-use
|
|
270
|
+
* gate releases exactly this use and the next one asks again, a conversation-scoped gate covers the rest
|
|
271
|
+
* of the conversation. The card says which, so the click is never wider than it reads. */
|
|
272
|
+
z.object({
|
|
273
|
+
kind: z.literal("credential_offer").describe("Releasing a credential the agent may only use once a named person says so."),
|
|
274
|
+
requestId: z.string().min(1).describe("Which card you are answering."),
|
|
275
|
+
approve: z
|
|
276
|
+
.boolean()
|
|
277
|
+
.describe(
|
|
278
|
+
"Yes releases it, as far as the card says (this one use, or the rest of the conversation). Only the people the card names can answer at all, yes or no.",
|
|
279
|
+
),
|
|
280
|
+
}),
|
|
262
281
|
]);
|
|
263
282
|
export type AgentReply = z.infer<typeof AgentReplySchema>;
|
|
264
283
|
// Steering: a user message delivered INTO the running turn (injected between tool calls, Claude Code style),
|
package/src/schemas/secrets.ts
CHANGED
|
@@ -16,6 +16,102 @@ export const SecretKeysSchema = z.object({
|
|
|
16
16
|
});
|
|
17
17
|
export const SecretKeyParamSchema = z.object({ key: z.string().describe("Which secret, by name.") });
|
|
18
18
|
export const SecretRevealSchema = z.object({ value: z.string().describe("The value itself. The only place in this API one is ever returned.") });
|
|
19
|
+
|
|
20
|
+
/* A CREDENTIAL THE AGENT MAY NOT USE UNTIL A NAMED PERSON SAYS SO, and who that person is.
|
|
21
|
+
*
|
|
22
|
+
* Masking already answers "can the model SEE this value" (no: it reads a `{{secret:name}}` reference), and
|
|
23
|
+
* the answer was enough while the only question was disclosure. It is not the whole question. A reference the
|
|
24
|
+
* model can write is a credential the model can SPEND, at whatever moment its own reasoning arrives at, and
|
|
25
|
+
* for the small set of credentials where one wrong spend is the incident — the production database's
|
|
26
|
+
* password, the company X account, the card-shaped connector — the owner wants the moment itself decided by a
|
|
27
|
+
* person, not the storage of the value. That is what a gate is: not a second lock on the vault, a REQUIRED
|
|
28
|
+
* CLICK in front of the exit.
|
|
29
|
+
*
|
|
30
|
+
* WHAT IT IS NOT A WALL AGAINST. A shell in this container runs as the workspace's owner and the policy file
|
|
31
|
+
* sits beside the vault it guards; anything that can read one can read the other, and a compromised container
|
|
32
|
+
* is out of scope here exactly as it is for the vault (SECURITY.md). The gate is a wall against the AGENT's
|
|
33
|
+
* own judgment being the last word — a prompt-injected model can ask for a release and cannot grant itself
|
|
34
|
+
* one — and against the ordinary accident of a turn reaching for the live credential when the staging one was
|
|
35
|
+
* meant.
|
|
36
|
+
*
|
|
37
|
+
* THE APPROVERS ARE AN EXACT LIST, not a role floor, and the owner is not on it implicitly. "Only Bob may
|
|
38
|
+
* release the production password" is the sentence people actually mean, and a floor cannot say it: a floor of
|
|
39
|
+
* `maintainer` says "anyone senior enough", which is the thing the owner was trying to narrow. So the list is
|
|
40
|
+
* the list, the owner adds themselves when they mean themselves, and a click from anybody else is refused
|
|
41
|
+
* server-side with the card left standing for whoever can. */
|
|
42
|
+
export const CredentialGateScopeSchema = z
|
|
43
|
+
.enum(["use", "conversation"])
|
|
44
|
+
.describe(
|
|
45
|
+
"How far one release goes: `use` asks again every single time (one click releases exactly one use), `conversation` covers the rest of this conversation and is forgotten when the daemon restarts.",
|
|
46
|
+
);
|
|
47
|
+
export type CredentialGateScope = z.infer<typeof CredentialGateScopeSchema>;
|
|
48
|
+
|
|
49
|
+
// What a gate is ABOUT: one stored secret by its reference name, or one whole connected capability (a
|
|
50
|
+
// connector's env, a browser account's profile, an MCP server) by its id. Two kinds rather than one because
|
|
51
|
+
// they are withheld differently — a secret is refused at the exit it would resolve at, a capability is simply
|
|
52
|
+
// never mounted for the turn — and the subject namespaces are disjoint anyway (env keys are SCREAMING_SNAKE).
|
|
53
|
+
export const CredentialGateKindSchema = z
|
|
54
|
+
.enum(["secret", "capability"])
|
|
55
|
+
.describe("Whether this gate covers one stored secret, by the name a reference carries, or one whole connected capability, by its id.");
|
|
56
|
+
export type CredentialGateKind = z.infer<typeof CredentialGateKindSchema>;
|
|
57
|
+
|
|
58
|
+
/* WHICH EXIT A RELEASE WAS ASKED FOR, so the card can say what is about to happen in the reader's terms
|
|
59
|
+
* rather than in the daemon's. The three secret exits are the ones the use ledger already names (a shell
|
|
60
|
+
* command, a JS run's script, a browser keystroke); `session` is a whole capability being mounted for the
|
|
61
|
+
* turn (a signed-in browser profile, a connector's env, an MCP server), and `otp` is one derived one-time
|
|
62
|
+
* code off a stored TOTP seed. */
|
|
63
|
+
export const CredentialLaneSchema = z
|
|
64
|
+
.enum(["shell", "code", "browser", "session", "otp"])
|
|
65
|
+
.describe("What the credential was about to be used for: a shell command, a script, typing into a page, mounting a connected account, or one one-time code.");
|
|
66
|
+
export type CredentialLane = z.infer<typeof CredentialLaneSchema>;
|
|
67
|
+
|
|
68
|
+
export const CredentialGateSchema = z.object({
|
|
69
|
+
// An env/generated KEY (`DATABASE_URL`), or a capability id (`reddit`, `komodo`). A vault name's field
|
|
70
|
+
// half (`reddit/password`) is deliberately NOT a subject: gating one field of a connected account and not
|
|
71
|
+
// its neighbours would be a gate with a hole in it, so the whole capability is the unit.
|
|
72
|
+
subject: z.string().min(1).describe("What is gated: a secret's name, or a connected capability's id."),
|
|
73
|
+
kind: CredentialGateKindSchema,
|
|
74
|
+
approvers: z
|
|
75
|
+
.array(z.string().min(3))
|
|
76
|
+
.min(1)
|
|
77
|
+
.describe(
|
|
78
|
+
"Exactly who may release it, by email, from the people on the Access roster. Not a seniority floor: nobody outside this list can release it, the owner included, unless the owner is on it.",
|
|
79
|
+
),
|
|
80
|
+
scope: CredentialGateScopeSchema,
|
|
81
|
+
});
|
|
82
|
+
export type CredentialGate = z.infer<typeof CredentialGateSchema>;
|
|
83
|
+
|
|
84
|
+
export const CredentialGatesSchema = z.object({
|
|
85
|
+
gates: z
|
|
86
|
+
.array(CredentialGateSchema)
|
|
87
|
+
.describe("Every gate in force. Names, subjects and approver addresses only: this answer never carries a credential."),
|
|
88
|
+
});
|
|
89
|
+
export type CredentialGates = z.infer<typeof CredentialGatesSchema>;
|
|
90
|
+
|
|
91
|
+
export const CredentialGateSubjectParamSchema = z.object({
|
|
92
|
+
subject: z.string().min(1).describe("Which gate, by the secret name or capability id it covers."),
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
/* THE AGENT'S OWN DOOR, `secrets request <subject> --why "…"`. A gated capability is ABSENT from a turn
|
|
96
|
+
* rather than refused inside it (its profile is not mounted, its env is not exported), so the model cannot
|
|
97
|
+
* discover the gate by tripping over it the way a secret reference does — it would conclude the account is
|
|
98
|
+
* disconnected and go looking for another road. This route is the road: it raises the same card the exits
|
|
99
|
+
* raise, and a release is worded as a grant for the REST OF THE CONVERSATION, because a session-shaped
|
|
100
|
+
* credential cannot be handed out for one use. */
|
|
101
|
+
export const CredentialRequestSchema = z.object({
|
|
102
|
+
subject: z.string().min(1).describe("What to ask for: the secret's name, or the connected capability's id."),
|
|
103
|
+
why: z.string().max(280).optional().describe("One line on what it is for. The only words on the card that are the agent's."),
|
|
104
|
+
// Which conversation's chat the card goes up in, filled by the CLI from the turn's own environment, so
|
|
105
|
+
// the model cannot aim a card at somebody else's conversation.
|
|
106
|
+
conversationId: z.string().optional().describe("Which conversation to raise the card in. The CLI fills this from the running turn."),
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
export const CredentialGrantSchema = z.object({
|
|
110
|
+
granted: z.literal(true).describe("Always true: a refusal is an error with a sentence, never a `false` here."),
|
|
111
|
+
approvedBy: z.string().describe("Who released it."),
|
|
112
|
+
message: z.string().describe("What the grant means in practice, and what to do next."),
|
|
113
|
+
});
|
|
114
|
+
|
|
19
115
|
// One entry per secret the sandbox knows about, across every store: intent env secrets and intentic-generated
|
|
20
116
|
// passwords (from the desired-state repo), capability credentials, and AI-provider accounts. Values never ride
|
|
21
117
|
// this shape, `revealable` says whether `reveal` can return one (everything but provider accounts).
|
|
@@ -56,9 +152,23 @@ export const SecretInventoryEntrySchema = z.object({
|
|
|
56
152
|
.string()
|
|
57
153
|
.optional()
|
|
58
154
|
.describe("Where it went: the start of the command or script, or the site. Names and destinations only, never values."),
|
|
155
|
+
// Who released it, when a gate made that use somebody's decision. Absent on an ungated use, which
|
|
156
|
+
// is nearly all of them: the ledger row records a person only where a person was actually asked.
|
|
157
|
+
approvedBy: z.string().optional().describe("Who released it for that use, when it is gated. Absent when nothing had to be approved."),
|
|
59
158
|
})
|
|
60
159
|
.optional()
|
|
61
160
|
.describe("The last time an agent actually spent this secret. Absent while it never has been, which most never are."),
|
|
161
|
+
/* THE APPROVAL THIS ENTRY IS BEHIND, joined on from the gate policy so the row can say "needs approval
|
|
162
|
+
* from Bob" without a second call. Absent on the overwhelming majority of entries: gating is for the few
|
|
163
|
+
* credentials where one wrong use is the incident, and a sandbox where everything asks is a sandbox where
|
|
164
|
+
* nobody reads the cards. */
|
|
165
|
+
gate: z
|
|
166
|
+
.object({
|
|
167
|
+
approvers: z.array(z.string()).describe("Who may release it, by email. Nobody else can, whatever their role."),
|
|
168
|
+
scope: CredentialGateScopeSchema,
|
|
169
|
+
})
|
|
170
|
+
.optional()
|
|
171
|
+
.describe("Who has to release this before the agent can use it, and for how long one release lasts. Absent when it is not gated."),
|
|
62
172
|
});
|
|
63
173
|
export type SecretInventoryEntry = z.infer<typeof SecretInventoryEntrySchema>;
|
|
64
174
|
export const SecretInventorySchema = z.object({
|
package/src/schemas/settings.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
// settings: per-sandbox agent settings (.intentic/config/settings.json)
|
|
2
2
|
import { z } from "zod";
|
|
3
|
+
import { CommandJudgeModeSchema } from "../safety-policy.js";
|
|
3
4
|
import { AdmissionPolicySchema, AdmissionRuleSchema, AgentRunPinSchema } from "./agent.js";
|
|
4
5
|
// Which prompt the agent is, before this turn composes anything on top. Two built-in bases and an escape
|
|
5
6
|
// hatch: Intentic's own (the default), Claude Code's preset, or the owner's text. Declared out here rather
|
|
@@ -715,17 +716,50 @@ export const SandboxSettingsSchema = z.object({
|
|
|
715
716
|
.record(z.string(), AdmissionRuleSchema)
|
|
716
717
|
.default({})
|
|
717
718
|
.describe("What an agent may do out in the world, per kind of action: go ahead, ask first, or never."),
|
|
718
|
-
/* THE COMMAND GATE
|
|
719
|
-
*
|
|
720
|
-
*
|
|
721
|
-
*
|
|
722
|
-
*
|
|
723
|
-
*
|
|
724
|
-
*
|
|
725
|
-
*
|
|
726
|
-
*
|
|
727
|
-
*
|
|
728
|
-
*
|
|
719
|
+
/* NO VERDICT FOR THE COMMAND GATE LIVES HERE, and the two settings that do are about the JUDGE rather than
|
|
720
|
+
* about what it should decide. `commandRules` used to be a verdict per CommandClass and `explainCommands`
|
|
721
|
+
* decided whether a card carried a sentence; both are gone, and the reason is the whole safety redesign
|
|
722
|
+
* (safety-policy.ts argues it). A regex verdict per class asked about `echo "rm -rf /"` and an actual delete
|
|
723
|
+
* in the same words, and no setting of six switches fixes that, because telling the two apart is an act of
|
|
724
|
+
* understanding rather than a threshold. What replaced them is the owner's written policy at
|
|
725
|
+
* .intentic/config/safety.md, read by a judge that also sees what the daemon knows about the turn, plus one
|
|
726
|
+
* typed hard rule the judge cannot waive. The Safety page edits that document; what a command may DO is
|
|
727
|
+
* settled there and nowhere in this object.
|
|
728
|
+
*
|
|
729
|
+
* WHETHER THE JUDGE RUNS AT ALL is a different question, and one this object has to answer, because it is
|
|
730
|
+
* the only tier of the design that spends money and interrupts people. An owner who does not want either is
|
|
731
|
+
* entitled to say so, and before this they could not: the old rulebook could be set to allow everything, and
|
|
732
|
+
* the redesign quietly made itself the one part of the sandbox you could only opt further into.
|
|
733
|
+
*
|
|
734
|
+
* Judged commands are also the one automatic job whose model choice genuinely differs from the rest of
|
|
735
|
+
* `quickModel`'s work, which is why the list below exists rather than a line in the comment above it: a
|
|
736
|
+
* commit message written by a model that misread the diff is a sentence somebody edits, and a verdict
|
|
737
|
+
* written by a model that misread a command is a card that should not have been raised or, worse, one that
|
|
738
|
+
* should have been. */
|
|
739
|
+
commandJudge: CommandJudgeModeSchema.default("on").describe(
|
|
740
|
+
"Whether a model reads your safety policy before a flagged command runs. Off judges nothing and asks about nothing; Watch judges everything and records it without ever interrupting you, which is how you find out what your policy actually does before you let it stop anything; On lets the verdict decide. Wiping a disk or deleting under /history asks at every setting — that rule is typed rather than judged, and cannot be turned off.",
|
|
741
|
+
),
|
|
742
|
+
/* WHICH MODEL READS THE POLICY, an ordered list of `${provider}:${modelId}` keys (quickModelKey) walked top
|
|
743
|
+
* to bottom, or EMPTY for whatever the quick model would be.
|
|
744
|
+
*
|
|
745
|
+
* A LIST, for the reason every other model setting here is one: the head runs out and the whole feature goes
|
|
746
|
+
* with it. Falling back to the quick chain rather than to Auto is deliberate — it is what this did before the
|
|
747
|
+
* setting existed, so an owner who never opens the row keeps exactly the behaviour they had, and one who
|
|
748
|
+
* writes an entry is saying that command verdicts are worth a different model than commit messages.
|
|
749
|
+
*
|
|
750
|
+
* THE ARGUMENT FOR SETTING IT AT ALL, since the cheapest connected rung is the default: this prompt is the
|
|
751
|
+
* one quick job that is genuinely adversarial. Its input includes text the agent is about to run, which may
|
|
752
|
+
* have arrived from a stranger's web page, and a small model can be talked round by it (command-judge.ts is
|
|
753
|
+
* candid about that). It is also the job where being WRONG is expensive in both directions — a needless card
|
|
754
|
+
* teaches the owner to click through the next one. Neither is a reason for us to spend somebody's frontier
|
|
755
|
+
* allowance by default; both are reasons for them to be able to. */
|
|
756
|
+
commandJudgeModels: z
|
|
757
|
+
.array(z.string())
|
|
758
|
+
.max(10)
|
|
759
|
+
.default([])
|
|
760
|
+
.describe(
|
|
761
|
+
"Which models decide whether a flagged command should run, tried in order so one spent account does not take the gate down. Empty uses whatever the quick model is, which is what this did before the setting existed.",
|
|
762
|
+
),
|
|
729
763
|
/* HOW MUCH AN AGENT MAY DELEGATE, the three ceilings the Claude Code harness enforces on its own Agent
|
|
730
764
|
* tool, surfaced here because their defaults are tuned for a laptop and this is a container the owner sized.
|
|
731
765
|
*
|
|
@@ -392,6 +392,53 @@ describe("foldTurn", () => {
|
|
|
392
392
|
]);
|
|
393
393
|
});
|
|
394
394
|
|
|
395
|
+
/* A GATED CREDENTIAL's card keeps WHO released it, not merely that something was approved: the approver is
|
|
396
|
+
* the whole point of the gate, and the reply cannot carry them (it is the daemon that verified the
|
|
397
|
+
* identity), so the receipt frame is the only place that name ever appears. */
|
|
398
|
+
it("keeps who released a gated credential on the card that asked for it", () => {
|
|
399
|
+
const offer = {
|
|
400
|
+
subject: "DATABASE_URL",
|
|
401
|
+
kind: "secret" as const,
|
|
402
|
+
lane: "shell" as const,
|
|
403
|
+
detail: "psql {{secret:DATABASE_URL}}",
|
|
404
|
+
why: "run the migration",
|
|
405
|
+
approvers: ["bob@corp.com"],
|
|
406
|
+
scope: "use" as const,
|
|
407
|
+
};
|
|
408
|
+
const events: AgentEvent[] = [
|
|
409
|
+
{ kind: "credential_offer", requestId: "c1", offer },
|
|
410
|
+
{ kind: "resolved", requestId: "c1", reply: { kind: "credential_offer", requestId: "c1", approve: true } },
|
|
411
|
+
{ kind: "credential_receipt", requestId: "c1", outcome: "released", approvedBy: "bob@corp.com" },
|
|
412
|
+
{ kind: "delta", text: "Migrated." },
|
|
413
|
+
];
|
|
414
|
+
expect(foldOf("migrate", events).slice(1)).toEqual([
|
|
415
|
+
{
|
|
416
|
+
role: "assistant",
|
|
417
|
+
text: "",
|
|
418
|
+
credentialOffer: { requestId: "c1", offer, status: "approved", receipt: { outcome: "released", approvedBy: "bob@corp.com" } },
|
|
419
|
+
},
|
|
420
|
+
{ role: "assistant", text: "Migrated." },
|
|
421
|
+
]);
|
|
422
|
+
});
|
|
423
|
+
|
|
424
|
+
/* A release nobody answered is nobody's refusal. The deadline passing freezes the card `cancelled` and
|
|
425
|
+
* writes NO receipt, because "refused" would name a decision a person never made — the same split the
|
|
426
|
+
* gate's two refusal sentences make to the agent. */
|
|
427
|
+
it("freezes an unanswered release as nobody's decision, with no receipt", () => {
|
|
428
|
+
const offer = {
|
|
429
|
+
subject: "reddit",
|
|
430
|
+
kind: "capability" as const,
|
|
431
|
+
lane: "session" as const,
|
|
432
|
+
approvers: ["alice@corp.com"],
|
|
433
|
+
scope: "conversation" as const,
|
|
434
|
+
};
|
|
435
|
+
const events: AgentEvent[] = [{ kind: "credential_offer", requestId: "c1", offer }];
|
|
436
|
+
expect(foldTurn(openingOf("post it"), events, "stopped").slice(1)).toEqual([
|
|
437
|
+
{ role: "assistant", text: "", credentialOffer: { requestId: "c1", offer, status: "cancelled" } },
|
|
438
|
+
{ role: "notice", text: "Stopped." },
|
|
439
|
+
]);
|
|
440
|
+
});
|
|
441
|
+
|
|
395
442
|
// A turn the user stopped says so, after freezing whatever it was parked on.
|
|
396
443
|
it("writes a stop down after cancelling what the turn was waiting on", () => {
|
|
397
444
|
const events: AgentEvent[] = [
|
package/src/transcript-fold.ts
CHANGED
|
@@ -407,6 +407,8 @@ export class TranscriptFold {
|
|
|
407
407
|
return this.park(event.requestId, { capabilityOffer: { requestId: event.requestId, offer: event.offer, status: "pending" } });
|
|
408
408
|
case "payment_offer":
|
|
409
409
|
return this.park(event.requestId, { paymentOffer: { requestId: event.requestId, offer: event.offer, status: "pending" } });
|
|
410
|
+
case "credential_offer":
|
|
411
|
+
return this.park(event.requestId, { credentialOffer: { requestId: event.requestId, offer: event.offer, status: "pending" } });
|
|
410
412
|
case "resolved":
|
|
411
413
|
// The card above was released, and the frame says how. The window that answered already froze its
|
|
412
414
|
// own card the instant its reply was accepted (card-status.ts, the same derivation), so this is a
|
|
@@ -441,6 +443,15 @@ export class TranscriptFold {
|
|
|
441
443
|
};
|
|
442
444
|
}
|
|
443
445
|
});
|
|
446
|
+
case "credential_receipt":
|
|
447
|
+
return this.patchParked(event.requestId, (row) => {
|
|
448
|
+
if (row.credentialOffer !== undefined) {
|
|
449
|
+
row.credentialOffer.receipt = {
|
|
450
|
+
outcome: event.outcome,
|
|
451
|
+
...(event.approvedBy === undefined ? {} : { approvedBy: event.approvedBy }),
|
|
452
|
+
};
|
|
453
|
+
}
|
|
454
|
+
});
|
|
444
455
|
// Facts about the turn, not rows in it (TURN_FACT_KINDS): the run relays them as themselves.
|
|
445
456
|
case "session":
|
|
446
457
|
case "init":
|