@intentic/sandbox-contract 1.240.0 → 1.242.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 +52 -16
- package/dist/agent-catalog.d.ts +5 -28
- package/dist/agent-catalog.d.ts.map +1 -1
- package/dist/agent-catalog.js +21 -157
- package/dist/agent-catalog.js.map +1 -1
- package/dist/agent-run-model.d.ts +3 -2
- package/dist/agent-run-model.d.ts.map +1 -1
- package/dist/agent-run-model.js +9 -6
- package/dist/agent-run-model.js.map +1 -1
- package/dist/agent-runtimes.d.ts +27 -0
- package/dist/agent-runtimes.d.ts.map +1 -0
- package/dist/agent-runtimes.js +113 -0
- package/dist/agent-runtimes.js.map +1 -0
- package/dist/approvals-execution.d.ts +11 -0
- package/dist/approvals-execution.d.ts.map +1 -0
- package/dist/approvals-execution.js +44 -0
- package/dist/approvals-execution.js.map +1 -0
- package/dist/arrival.d.ts +151 -0
- package/dist/arrival.d.ts.map +1 -0
- package/dist/arrival.js +56 -0
- package/dist/arrival.js.map +1 -0
- package/dist/card-status.d.ts +5 -0
- package/dist/card-status.d.ts.map +1 -0
- package/dist/card-status.js +74 -0
- package/dist/card-status.js.map +1 -0
- package/dist/chores/chores.d.ts.map +1 -1
- package/dist/chores/chores.js +1 -1
- package/dist/chores/chores.js.map +1 -1
- package/dist/command-classes.d.ts +0 -1
- package/dist/command-classes.d.ts.map +1 -1
- package/dist/command-classes.js +9 -2
- package/dist/command-classes.js.map +1 -1
- package/dist/command-run.d.ts +10 -0
- package/dist/command-run.d.ts.map +1 -0
- package/dist/command-run.js +46 -0
- package/dist/command-run.js.map +1 -0
- package/dist/contracts/agent.contract.d.ts +991 -771
- package/dist/contracts/agent.contract.d.ts.map +1 -1
- package/dist/contracts/agents.contract.d.ts +345 -337
- package/dist/contracts/agents.contract.d.ts.map +1 -1
- package/dist/contracts/agents.contract.js +20 -2
- package/dist/contracts/agents.contract.js.map +1 -1
- package/dist/contracts/approvals.contract.d.ts +99 -0
- package/dist/contracts/approvals.contract.d.ts.map +1 -0
- package/dist/contracts/approvals.contract.js +32 -0
- package/dist/contracts/approvals.contract.js.map +1 -0
- package/dist/contracts/ci.contract.d.ts +1 -4
- package/dist/contracts/ci.contract.d.ts.map +1 -1
- package/dist/contracts/ci.contract.js +1 -9
- package/dist/contracts/ci.contract.js.map +1 -1
- package/dist/contracts/claude.contract.d.ts +9 -0
- package/dist/contracts/claude.contract.d.ts.map +1 -1
- package/dist/contracts/cursor.contract.d.ts +6 -0
- package/dist/contracts/cursor.contract.d.ts.map +1 -1
- package/dist/contracts/git.contract.d.ts +63 -1
- package/dist/contracts/git.contract.d.ts.map +1 -1
- package/dist/contracts/git.contract.js +22 -4
- package/dist/contracts/git.contract.js.map +1 -1
- package/dist/contracts/grok.contract.d.ts +3 -0
- package/dist/contracts/grok.contract.d.ts.map +1 -1
- package/dist/contracts/keys.contract.d.ts +81 -0
- package/dist/contracts/keys.contract.d.ts.map +1 -0
- package/dist/contracts/keys.contract.js +51 -0
- package/dist/contracts/keys.contract.js.map +1 -0
- package/dist/contracts/panels.contract.d.ts +7 -0
- package/dist/contracts/panels.contract.d.ts.map +1 -1
- package/dist/contracts/prepush.contract.js +2 -2
- package/dist/contracts/providers.contract.d.ts +2 -0
- package/dist/contracts/providers.contract.d.ts.map +1 -1
- package/dist/contracts/runner.contract.d.ts +84 -86
- package/dist/contracts/runner.contract.d.ts.map +1 -1
- package/dist/contracts/safety.contract.d.ts +35 -0
- package/dist/contracts/safety.contract.d.ts.map +1 -0
- package/dist/contracts/safety.contract.js +32 -0
- package/dist/contracts/safety.contract.js.map +1 -0
- package/dist/contracts/sessions.contract.d.ts +73 -337
- package/dist/contracts/sessions.contract.d.ts.map +1 -1
- package/dist/contracts/settings.contract.d.ts +30 -32
- package/dist/contracts/settings.contract.d.ts.map +1 -1
- package/dist/contracts/system.contract.d.ts +139 -337
- package/dist/contracts/system.contract.d.ts.map +1 -1
- package/dist/contracts/translator.contract.d.ts +31 -3
- package/dist/contracts/translator.contract.d.ts.map +1 -1
- package/dist/contracts/usage.contract.d.ts +29 -20
- package/dist/contracts/usage.contract.d.ts.map +1 -1
- package/dist/contracts/usage.contract.js +13 -0
- package/dist/contracts/usage.contract.js.map +1 -1
- package/dist/contracts/workspace.contract.d.ts +8 -0
- package/dist/contracts/workspace.contract.d.ts.map +1 -1
- package/dist/conversation-ids.d.ts +2 -0
- package/dist/conversation-ids.d.ts.map +1 -1
- package/dist/conversation-ids.js +8 -0
- package/dist/conversation-ids.js.map +1 -1
- package/dist/credential-material.d.ts +1 -0
- package/dist/credential-material.d.ts.map +1 -1
- package/dist/credential-material.js +24 -2
- package/dist/credential-material.js.map +1 -1
- package/dist/definition.d.ts +51 -93
- package/dist/definition.d.ts.map +1 -1
- package/dist/definition.js +6 -28
- package/dist/definition.js.map +1 -1
- package/dist/events.d.ts +1831 -2285
- package/dist/events.d.ts.map +1 -1
- package/dist/events.js +155 -55
- package/dist/events.js.map +1 -1
- package/dist/history-state.d.ts.map +1 -1
- package/dist/history-state.js +1 -0
- package/dist/history-state.js.map +1 -1
- package/dist/index.d.ts +1959 -1903
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +20 -5
- package/dist/index.js.map +1 -1
- package/dist/ingress-protocol.d.ts +13 -1
- package/dist/ingress-protocol.d.ts.map +1 -1
- package/dist/ingress-protocol.js +19 -0
- package/dist/ingress-protocol.js.map +1 -1
- package/dist/mentions.d.ts +3 -0
- package/dist/mentions.d.ts.map +1 -0
- package/dist/mentions.js +13 -0
- package/dist/mentions.js.map +1 -0
- package/dist/needs-action.d.ts +7 -0
- package/dist/needs-action.d.ts.map +1 -0
- package/dist/needs-action.js +3 -0
- package/dist/needs-action.js.map +1 -0
- package/dist/overlay-lint.d.ts +7 -0
- package/dist/overlay-lint.d.ts.map +1 -0
- package/dist/overlay-lint.js +68 -0
- package/dist/overlay-lint.js.map +1 -0
- package/dist/owner-ticket.d.ts +18 -0
- package/dist/owner-ticket.d.ts.map +1 -0
- package/dist/owner-ticket.js +51 -0
- package/dist/owner-ticket.js.map +1 -0
- package/dist/plan-pools.d.ts +12 -0
- package/dist/plan-pools.d.ts.map +1 -0
- package/dist/plan-pools.js +30 -0
- package/dist/plan-pools.js.map +1 -0
- package/dist/provider-specs.d.ts +228 -0
- package/dist/provider-specs.d.ts.map +1 -0
- package/dist/provider-specs.js +124 -0
- package/dist/provider-specs.js.map +1 -0
- package/dist/quick-model.d.ts.map +1 -1
- package/dist/quick-model.js +2 -1
- package/dist/quick-model.js.map +1 -1
- package/dist/runner-protocol.js +2 -2
- package/dist/runner-protocol.js.map +1 -1
- package/dist/runtime-state.d.ts +2 -2
- package/dist/runtime-state.d.ts.map +1 -1
- package/dist/runtime-state.js +1 -1
- package/dist/runtime-state.js.map +1 -1
- package/dist/safety-policy.d.ts +49 -0
- package/dist/safety-policy.d.ts.map +1 -0
- package/dist/safety-policy.js +52 -0
- package/dist/safety-policy.js.map +1 -0
- package/dist/schemas/agent.d.ts +21 -3
- package/dist/schemas/agent.d.ts.map +1 -1
- package/dist/schemas/agent.js +18 -8
- package/dist/schemas/agent.js.map +1 -1
- package/dist/schemas/agents.d.ts +19 -0
- package/dist/schemas/agents.d.ts.map +1 -1
- package/dist/schemas/agents.js +15 -0
- package/dist/schemas/agents.js.map +1 -1
- package/dist/schemas/approvals.d.ts +241 -0
- package/dist/schemas/approvals.d.ts.map +1 -0
- package/dist/schemas/approvals.js +63 -0
- package/dist/schemas/approvals.js.map +1 -0
- package/dist/schemas/automations.d.ts +5 -0
- package/dist/schemas/automations.d.ts.map +1 -1
- package/dist/schemas/ci.d.ts +5 -9
- package/dist/schemas/ci.d.ts.map +1 -1
- package/dist/schemas/ci.js +3 -10
- package/dist/schemas/ci.js.map +1 -1
- package/dist/schemas/computers.d.ts +31 -5
- package/dist/schemas/computers.d.ts.map +1 -1
- package/dist/schemas/computers.js +7 -5
- package/dist/schemas/computers.js.map +1 -1
- package/dist/schemas/environment.d.ts +11 -131
- package/dist/schemas/environment.d.ts.map +1 -1
- package/dist/schemas/environment.js +5 -42
- package/dist/schemas/environment.js.map +1 -1
- package/dist/schemas/git-history.d.ts +4 -0
- package/dist/schemas/git-history.d.ts.map +1 -1
- package/dist/schemas/git.d.ts +179 -0
- package/dist/schemas/git.d.ts.map +1 -1
- package/dist/schemas/git.js +39 -1
- package/dist/schemas/git.js.map +1 -1
- package/dist/schemas/panels.d.ts +21 -0
- package/dist/schemas/panels.d.ts.map +1 -1
- package/dist/schemas/panels.js +5 -0
- package/dist/schemas/panels.js.map +1 -1
- package/dist/schemas/plan-limits.d.ts +50 -3
- package/dist/schemas/plan-limits.d.ts.map +1 -1
- package/dist/schemas/plan-limits.js +11 -6
- package/dist/schemas/plan-limits.js.map +1 -1
- package/dist/schemas/provider-oauth.d.ts +6 -0
- package/dist/schemas/provider-oauth.d.ts.map +1 -1
- package/dist/schemas/provider-subscriptions.d.ts +2 -1
- package/dist/schemas/provider-subscriptions.d.ts.map +1 -1
- package/dist/schemas/provider-subscriptions.js +2 -1
- package/dist/schemas/provider-subscriptions.js.map +1 -1
- package/dist/schemas/settings.d.ts +29 -16
- package/dist/schemas/settings.d.ts.map +1 -1
- package/dist/schemas/settings.js +13 -20
- package/dist/schemas/settings.js.map +1 -1
- package/dist/schemas/system.d.ts +3 -0
- package/dist/schemas/system.d.ts.map +1 -1
- package/dist/schemas/system.js +5 -1
- package/dist/schemas/system.js.map +1 -1
- package/dist/schemas/workspace-repos.d.ts +14 -0
- package/dist/schemas/workspace-repos.d.ts.map +1 -1
- package/dist/schemas/workspace-repos.js +5 -0
- package/dist/schemas/workspace-repos.js.map +1 -1
- package/dist/schemas/workspace-tree.d.ts +1 -0
- package/dist/schemas/workspace-tree.d.ts.map +1 -1
- package/dist/schemas/workspace-tree.js +3 -0
- package/dist/schemas/workspace-tree.js.map +1 -1
- package/dist/transcript-fold.d.ts +31 -0
- package/dist/transcript-fold.d.ts.map +1 -0
- package/dist/transcript-fold.js +457 -0
- package/dist/transcript-fold.js.map +1 -0
- package/dist/workspace-state.d.ts +20 -3
- package/dist/workspace-state.d.ts.map +1 -1
- package/dist/workspace-state.js +22 -2
- package/dist/workspace-state.js.map +1 -1
- package/package.json +27 -5
- package/src/agent-catalog.test.ts +1 -1
- package/src/agent-catalog.ts +76 -555
- package/src/agent-run-model.test.ts +31 -15
- package/src/agent-run-model.ts +26 -12
- package/src/agent-runtimes.ts +419 -0
- package/src/approvals-execution.ts +96 -0
- package/src/arrival.ts +160 -0
- package/src/capability-ledger.test.ts +6 -4
- package/src/card-status.ts +93 -0
- package/src/chores/chores.ts +6 -3
- package/src/command-classes.test.ts +25 -0
- package/src/command-classes.ts +47 -5
- package/src/command-run.ts +78 -0
- package/src/contract-lock.test.ts +1 -1
- package/src/contract-lock.ts +2 -2
- package/src/contracts/agents.contract.ts +35 -2
- package/src/contracts/approvals.contract.ts +37 -0
- package/src/contracts/ci.contract.ts +0 -13
- package/src/contracts/git.contract.ts +28 -3
- package/src/contracts/keys.contract.ts +79 -0
- package/src/contracts/prepush.contract.ts +3 -3
- package/src/contracts/safety.contract.ts +48 -0
- package/src/contracts/usage.contract.ts +22 -0
- package/src/conversation-ids.test.ts +31 -1
- package/src/conversation-ids.ts +37 -3
- package/src/credential-material.test.ts +69 -1
- package/src/credential-material.ts +86 -5
- package/src/definition.ts +25 -53
- package/src/events.ts +374 -173
- package/src/history-state.ts +6 -0
- package/src/index.ts +24 -5
- package/src/ingress-protocol.ts +52 -1
- package/src/mentions.test.ts +22 -0
- package/src/mentions.ts +25 -0
- package/src/needs-action.ts +14 -0
- package/src/overlay-lint.test.ts +89 -0
- package/src/overlay-lint.ts +116 -0
- package/src/owner-ticket.test.ts +54 -0
- package/src/owner-ticket.ts +102 -0
- package/src/plan-pools.test.ts +71 -0
- package/src/plan-pools.ts +82 -0
- package/src/provider-specs.test.ts +130 -0
- package/src/provider-specs.ts +352 -0
- package/src/quick-model.ts +9 -5
- package/src/runner-protocol.ts +2 -2
- package/src/runtime-state.ts +6 -6
- package/src/safety-policy.ts +165 -0
- package/src/schemas/agent.ts +62 -17
- package/src/schemas/agents.ts +71 -0
- package/src/schemas/approvals.ts +174 -0
- package/src/schemas/ci.ts +19 -26
- package/src/schemas/computers.ts +52 -11
- package/src/schemas/environment.ts +21 -100
- package/src/schemas/git.ts +121 -6
- package/src/schemas/issues.ts +1 -1
- package/src/schemas/panels.ts +20 -0
- package/src/schemas/plan-limits.ts +58 -11
- package/src/schemas/provider-subscriptions.ts +7 -2
- package/src/schemas/settings.ts +67 -93
- package/src/schemas/system.ts +12 -1
- package/src/schemas/workspace-repos.ts +9 -0
- package/src/schemas/workspace-tree.ts +11 -0
- package/src/transcript-fold.test.ts +493 -0
- package/src/transcript-fold.ts +655 -0
- package/src/workspace-state.test.ts +77 -29
- package/src/workspace-state.ts +99 -31
- package/dist/contracts/drafts.contract.d.ts +0 -57
- package/dist/contracts/drafts.contract.d.ts.map +0 -1
- package/dist/contracts/drafts.contract.js +0 -32
- package/dist/contracts/drafts.contract.js.map +0 -1
- package/dist/definition-action.d.ts +0 -7
- package/dist/definition-action.d.ts.map +0 -1
- package/dist/definition-action.js +0 -3
- package/dist/definition-action.js.map +0 -1
- package/dist/publish-drafts.d.ts +0 -7
- package/dist/publish-drafts.d.ts.map +0 -1
- package/dist/publish-drafts.js +0 -20
- package/dist/publish-drafts.js.map +0 -1
- package/dist/schemas/drafts.d.ts +0 -84
- package/dist/schemas/drafts.d.ts.map +0 -1
- package/dist/schemas/drafts.js +0 -44
- package/dist/schemas/drafts.js.map +0 -1
- package/src/contracts/drafts.contract.ts +0 -35
- package/src/definition-action.ts +0 -8
- package/src/publish-drafts.ts +0 -59
- package/src/schemas/drafts.ts +0 -108
|
@@ -9,6 +9,7 @@ import {
|
|
|
9
9
|
AgentLandSchema,
|
|
10
10
|
AgentPlaceSchema,
|
|
11
11
|
AgentRenameSchema,
|
|
12
|
+
AgentResumeAfterLimitSchema,
|
|
12
13
|
AgentResumeAfterOutageSchema,
|
|
13
14
|
AgentsArchivedSchema,
|
|
14
15
|
AgentSearchQuerySchema,
|
|
@@ -19,7 +20,7 @@ import {
|
|
|
19
20
|
LandResultSchema,
|
|
20
21
|
} from "../schemas/agents.js";
|
|
21
22
|
import { AgentsListSchema } from "../schemas/automations.js";
|
|
22
|
-
import { AgentChangesSchema } from "../schemas/git.js";
|
|
23
|
+
import { AgentChangesSchema, AgentHistorySchema } from "../schemas/git.js";
|
|
23
24
|
import { FileDiffSchema } from "../schemas/history.js";
|
|
24
25
|
import { OkSchema } from "../schemas/shared.js";
|
|
25
26
|
|
|
@@ -100,7 +101,7 @@ export const agentsContract = {
|
|
|
100
101
|
.input(AgentIdSchema)
|
|
101
102
|
.output(AgentTranscriptSchema),
|
|
102
103
|
/* SPEAK AS THE AGENT, append the user's words to the conversation's record as an assistant row, with no
|
|
103
|
-
* turn behind them and no reply. The row is marked `placed` for human readers (
|
|
104
|
+
* turn behind them and no reply. The row is marked `placed` for human readers (TranscriptRowSchema); the
|
|
104
105
|
* provider session is FORGOTTEN in the same breath, rewind-style, so the next real turn opens a fresh
|
|
105
106
|
* runtime session seeded from the record, where the placed line reads as the agent's own words, because
|
|
106
107
|
* the handoff renders every assistant row identically. A running turn is CONFLICT: the illusion can only be
|
|
@@ -155,6 +156,23 @@ export const agentsContract = {
|
|
|
155
156
|
})
|
|
156
157
|
.input(AgentResumeAfterOutageSchema)
|
|
157
158
|
.output(AgentSummarySchema),
|
|
159
|
+
/* THIS conversation's answer to a spent allowance, the same override in the same shape as the outage one
|
|
160
|
+
* above; null clears it back to "inherit". Offered on the CARD as well as in the chat, which is the one
|
|
161
|
+
* placement difference worth stating: an outage is over in minutes and is met by whoever is in the room,
|
|
162
|
+
* while a limit reopens hours later, so the person deciding is usually looking at a board rather than at a
|
|
163
|
+
* transcript.
|
|
164
|
+
*
|
|
165
|
+
* Legal mid-turn, and for a workspace conversation, for the same two reasons its neighbour is. */
|
|
166
|
+
resumeAfterLimit: oc
|
|
167
|
+
.route({
|
|
168
|
+
method: "POST",
|
|
169
|
+
path: "/agents/{id}/resume-after-limit",
|
|
170
|
+
summary: "Whether this conversation sends itself again when its allowance comes back",
|
|
171
|
+
description:
|
|
172
|
+
"Overrides the sandbox-wide setting for one conversation; clear it to follow the default again. Off unless asked for, because the allowance is the user's own budget and a turn that spends it the moment it reopens is not a decision to make on their behalf.",
|
|
173
|
+
})
|
|
174
|
+
.input(AgentResumeAfterLimitSchema)
|
|
175
|
+
.output(AgentSummarySchema),
|
|
158
176
|
seen: oc
|
|
159
177
|
.route({
|
|
160
178
|
method: "POST",
|
|
@@ -204,6 +222,21 @@ export const agentsContract = {
|
|
|
204
222
|
})
|
|
205
223
|
.input(AgentIdSchema)
|
|
206
224
|
.output(AgentChangesSchema),
|
|
225
|
+
// The other side of `diff`: the work that is no longer a difference because it is in your own history, and
|
|
226
|
+
// the commits holding it. Asked for only once `diff` reports something absorbed, since it costs a `git log`
|
|
227
|
+
// per repo and answers nothing until then. Reading one of these files is the SAME call as reading a
|
|
228
|
+
// reviewable one (`fileDiff` below): the question "what did this conversation do to this file" has one
|
|
229
|
+
// answer whether or not you have since committed it.
|
|
230
|
+
history: oc
|
|
231
|
+
.route({
|
|
232
|
+
method: "GET",
|
|
233
|
+
path: "/agents/{id}/history",
|
|
234
|
+
summary: "Where a conversation's committed work lives",
|
|
235
|
+
description:
|
|
236
|
+
"The commits in your own history that carry this conversation's work, with the files each one brought. Use it when the change list is empty or short because you already committed what it wrote: those files are not differences against the main line any more, so they are not in the review, and this is where they went.",
|
|
237
|
+
})
|
|
238
|
+
.input(AgentIdSchema)
|
|
239
|
+
.output(AgentHistorySchema),
|
|
207
240
|
fileDiff: oc
|
|
208
241
|
.route({
|
|
209
242
|
method: "GET",
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { oc } from "@orpc/contract";
|
|
2
|
+
import { ApprovalIdParamSchema, ApprovalsListSchema, ApprovalSummarySchema } from "../schemas/approvals.js";
|
|
3
|
+
import { OkSchema } from "../schemas/shared.js";
|
|
4
|
+
|
|
5
|
+
// The sandbox's approvals queue (things the agent prepared and may not do until the owner says yes). The agent
|
|
6
|
+
// creates the files directly, these routes are the OWNER's side: `list` is the inbox, `upsert` covers approve /
|
|
7
|
+
// edit / reschedule / retry (all a plain re-post with a field changed, like the automations enabled toggle),
|
|
8
|
+
// `remove` is reject. The verbs are the same whatever the kind, which is the point of one queue.
|
|
9
|
+
export const approvalsContract = {
|
|
10
|
+
list: oc
|
|
11
|
+
.route({
|
|
12
|
+
method: "GET",
|
|
13
|
+
path: "/approvals",
|
|
14
|
+
summary: "Things waiting for your yes",
|
|
15
|
+
description:
|
|
16
|
+
"Everything an agent has prepared and would like to do: posts to publish, actions to carry out. Nothing here has happened yet.",
|
|
17
|
+
})
|
|
18
|
+
.output(ApprovalsListSchema),
|
|
19
|
+
upsert: oc
|
|
20
|
+
.route({
|
|
21
|
+
method: "POST",
|
|
22
|
+
path: "/approvals",
|
|
23
|
+
summary: "Approve, edit or retry one",
|
|
24
|
+
description: "All three are the same act with a different field changed, so they share one call. Send the item back as you want it.",
|
|
25
|
+
})
|
|
26
|
+
.input(ApprovalSummarySchema)
|
|
27
|
+
.output(OkSchema),
|
|
28
|
+
remove: oc
|
|
29
|
+
.route({
|
|
30
|
+
method: "DELETE",
|
|
31
|
+
path: "/approvals/{id}",
|
|
32
|
+
summary: "Reject one",
|
|
33
|
+
description: "Throws it away undone.",
|
|
34
|
+
})
|
|
35
|
+
.input(ApprovalIdParamSchema)
|
|
36
|
+
.output(OkSchema),
|
|
37
|
+
};
|
|
@@ -5,7 +5,6 @@ import {
|
|
|
5
5
|
CiJobsResponseSchema,
|
|
6
6
|
CiRunParamSchema,
|
|
7
7
|
CiRunsResponseSchema,
|
|
8
|
-
CiSeenResponseSchema,
|
|
9
8
|
} from "../schemas/ci.js";
|
|
10
9
|
import { OkSchema } from "../schemas/shared.js";
|
|
11
10
|
|
|
@@ -61,16 +60,4 @@ export const ciContract = {
|
|
|
61
60
|
})
|
|
62
61
|
.input(CiFixParamSchema)
|
|
63
62
|
.output(CiFixResponseSchema),
|
|
64
|
-
// "I have looked at the pipelines", what silences the rail badge for breakages already read. No input:
|
|
65
|
-
// the surface is read as a whole, and the daemon stamps its own clock so a skewed browser can't mark
|
|
66
|
-
// future failures as already seen.
|
|
67
|
-
seen: oc
|
|
68
|
-
.route({
|
|
69
|
-
method: "POST",
|
|
70
|
-
path: "/ci/seen",
|
|
71
|
-
summary: "Mark the pipelines as read",
|
|
72
|
-
description:
|
|
73
|
-
"Silences the badge for breakages already looked at. Takes nothing, because the view is read as a whole, and the daemon stamps its own clock so a browser with the wrong time cannot mark future failures as already seen.",
|
|
74
|
-
})
|
|
75
|
-
.output(CiSeenResponseSchema),
|
|
76
63
|
};
|
|
@@ -16,6 +16,7 @@ import {
|
|
|
16
16
|
GitRemoteStateSchema,
|
|
17
17
|
GitStageSchema,
|
|
18
18
|
GitStatusSchema,
|
|
19
|
+
PushRunSchema,
|
|
19
20
|
PushSchema,
|
|
20
21
|
} from "../schemas/git.js";
|
|
21
22
|
import {
|
|
@@ -452,16 +453,40 @@ export const gitContract = {
|
|
|
452
453
|
})
|
|
453
454
|
.input(RepoParamSchema)
|
|
454
455
|
.output(GitActionResultSchema),
|
|
456
|
+
/* THE PUSH IS A RUN, the pre-push check's three verbs over again (prepush.contract.ts), for the same reason:
|
|
457
|
+
* a push runs the repository's own pre-push hook, which for a workspace with a real gate is the whole suite,
|
|
458
|
+
* and a request held open for minutes dies at the first proxy and at the browser's own header deadline. So
|
|
459
|
+
* `push` starts it and answers at once, `pushState` is polled for the verdict, and the output is a terminal
|
|
460
|
+
* the owner can watch. Addressed by repo, unlike the check: there is one working tree but many remotes. */
|
|
455
461
|
push: oc
|
|
456
462
|
.route({
|
|
457
463
|
method: "POST",
|
|
458
464
|
path: "/git/{repo}/push",
|
|
459
|
-
summary: "
|
|
465
|
+
summary: "Start sending commits to the remote",
|
|
460
466
|
description:
|
|
461
|
-
"
|
|
467
|
+
"Starts pushing the current branch, setting its upstream on first push, and answers at once: the push runs in a real terminal (it runs this repository's pre-push hook, which can be a whole suite), so watch it there and poll pushState for the verdict. A second start while one is going joins it rather than pushing twice.",
|
|
462
468
|
})
|
|
463
469
|
.input(PushSchema)
|
|
464
|
-
.output(
|
|
470
|
+
.output(OkSchema),
|
|
471
|
+
pushState: oc
|
|
472
|
+
.route({
|
|
473
|
+
method: "GET",
|
|
474
|
+
path: "/git/{repo}/push",
|
|
475
|
+
summary: "How the push is going",
|
|
476
|
+
description:
|
|
477
|
+
"The verdict, or the progress so far: where it is, the terminal it runs in, and for a push that did not go, git's last words and who refused it, the repository's own pre-push hook, the remote, or the transport. Idle when nothing has been started for this repository.",
|
|
478
|
+
})
|
|
479
|
+
.input(RepoParamSchema)
|
|
480
|
+
.output(PushRunSchema),
|
|
481
|
+
pushCancel: oc
|
|
482
|
+
.route({
|
|
483
|
+
method: "POST",
|
|
484
|
+
path: "/git/{repo}/push/cancel",
|
|
485
|
+
summary: "Stop the push",
|
|
486
|
+
description: "Kills the run. It settles as cancelled; nothing that git had not already sent reaches the remote.",
|
|
487
|
+
})
|
|
488
|
+
.input(RepoParamSchema)
|
|
489
|
+
.output(OkSchema),
|
|
465
490
|
files: oc
|
|
466
491
|
.route({
|
|
467
492
|
method: "GET",
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import { oc } from "@orpc/contract";
|
|
2
|
+
import { z } from "zod";
|
|
3
|
+
import { KeyProviderParamSchema } from "../schemas/agent.js";
|
|
4
|
+
import { AccountIdSchema, AccountRenameSchema, OauthAccountListSchema, OauthAccountSchema } from "../schemas/provider-oauth.js";
|
|
5
|
+
import { OkSchema } from "../schemas/shared.js";
|
|
6
|
+
|
|
7
|
+
/* THE PROVIDERS YOU CONNECT BY PASTING A KEY (Sandbox ▸ Agent), on four routes with the provider as a PARAMETER
|
|
8
|
+
* rather than four routes each.
|
|
9
|
+
*
|
|
10
|
+
* The lesson providers.contract.ts already learned about model catalogs: five identical route files differing
|
|
11
|
+
* only in a name is a shape that costs a vertical slice through the contract, the router, the service container
|
|
12
|
+
* and every test double every time a provider is added. Here the providers genuinely are identical — a keyed
|
|
13
|
+
* provider is a base URL, a catalog URL and a store — so the id is a parameter and adding one is a spec row.
|
|
14
|
+
*
|
|
15
|
+
* WHY NOT THE OAUTH ROUTES. Claude and Cursor connect through a handshake this daemon runs: a `start` that
|
|
16
|
+
* returns something to open and an `exchange`/poll that completes it. There is no handshake here at all — the
|
|
17
|
+
* user already has the credential and hands it over — so `connect` takes the key and answers with the account
|
|
18
|
+
* it became. Sharing a route family with the OAuth providers would have meant a `start` that returns nothing
|
|
19
|
+
* and an `exchange` whose "code" is really a secret, which is a worse lie than two families.
|
|
20
|
+
*
|
|
21
|
+
* WHY NOT THE TRANSLATOR ROUTES. Those address subscriptions CLIProxyAPI holds; these credentials live in this
|
|
22
|
+
* daemon's own auth tree, and a disconnect here removes a file rather than asking a proxy to forget one.
|
|
23
|
+
*
|
|
24
|
+
* THE PATHS MIRROR THE OAUTH PROVIDERS' (`<base>/accounts`, `<base>/account/rename`,
|
|
25
|
+
* `<base>/account/disconnect`) with `/keys/<provider>` as the base, so the browser's account helpers reach
|
|
26
|
+
* these rows with the same three URLs they already build for Claude, Grok and Cursor. Only `connect`
|
|
27
|
+
* differs, because only `connect` is a different act.
|
|
28
|
+
*
|
|
29
|
+
* THE KEY TRAVELS IN, NEVER OUT. `connect` takes it and every answer on this contract is an OauthAccount, whose
|
|
30
|
+
* shape has no field a credential could ride in — which is what makes "does this leak the key" a question you
|
|
31
|
+
* answer by reading the schema instead of the handler. */
|
|
32
|
+
export const keysContract = {
|
|
33
|
+
accounts: oc
|
|
34
|
+
.route({
|
|
35
|
+
method: "GET",
|
|
36
|
+
path: "/keys/{provider}/accounts",
|
|
37
|
+
summary: "Connected accounts for a key-based provider",
|
|
38
|
+
description: "Which keys are connected for this provider. The keys themselves never travel: being in this list is what connected means.",
|
|
39
|
+
})
|
|
40
|
+
.input(KeyProviderParamSchema)
|
|
41
|
+
.output(OauthAccountListSchema),
|
|
42
|
+
connect: oc
|
|
43
|
+
.route({
|
|
44
|
+
method: "POST",
|
|
45
|
+
path: "/keys/{provider}/connect",
|
|
46
|
+
summary: "Connect an API key",
|
|
47
|
+
description:
|
|
48
|
+
"Stores an API key for this provider and answers with the account it became. A sandbox can hold several keys for one provider side by side.",
|
|
49
|
+
})
|
|
50
|
+
.input(
|
|
51
|
+
KeyProviderParamSchema.extend({
|
|
52
|
+
apiKey: z.string().min(1).describe("The key, as the provider issued it. Stored in this sandbox and never sent back out."),
|
|
53
|
+
label: z
|
|
54
|
+
.string()
|
|
55
|
+
.max(80)
|
|
56
|
+
.optional()
|
|
57
|
+
.describe("What to call it. Blank falls back to the provider's own name, which is all a pasted key can say about itself."),
|
|
58
|
+
}),
|
|
59
|
+
)
|
|
60
|
+
.output(OauthAccountSchema),
|
|
61
|
+
rename: oc
|
|
62
|
+
.route({
|
|
63
|
+
method: "POST",
|
|
64
|
+
path: "/keys/{provider}/account/rename",
|
|
65
|
+
summary: "Rename one connected key",
|
|
66
|
+
description: "Nothing in a pasted key says whose it is, so naming it is the only way to tell two of them apart.",
|
|
67
|
+
})
|
|
68
|
+
.input(KeyProviderParamSchema.extend(AccountRenameSchema.shape))
|
|
69
|
+
.output(OkSchema),
|
|
70
|
+
disconnect: oc
|
|
71
|
+
.route({
|
|
72
|
+
method: "POST",
|
|
73
|
+
path: "/keys/{provider}/account/disconnect",
|
|
74
|
+
summary: "Disconnect one key",
|
|
75
|
+
description: "Removes a single stored key. Any others under the same provider stay connected.",
|
|
76
|
+
})
|
|
77
|
+
.input(KeyProviderParamSchema.extend(AccountIdSchema.shape))
|
|
78
|
+
.output(OkSchema),
|
|
79
|
+
};
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import { oc } from "@orpc/contract";
|
|
2
|
-
import {
|
|
2
|
+
import { CommandRunSchema } from "../schemas/ci.js";
|
|
3
3
|
import { OkSchema } from "../schemas/shared.js";
|
|
4
4
|
|
|
5
5
|
// The pre-push check, the command the workspace runs when the user pushes, before anything leaves the machine
|
|
6
|
-
// (see
|
|
6
|
+
// (see CommandRunSchema for where this sits and why). Three verbs about ONE run: the check answers about the
|
|
7
7
|
// main working tree, of which there is exactly one, so nothing here is addressed by id.
|
|
8
8
|
//
|
|
9
9
|
// `run` starts the check and returns immediately, a suite takes minutes, and an oRPC call held open for one
|
|
@@ -20,7 +20,7 @@ export const prepushContract = {
|
|
|
20
20
|
description:
|
|
21
21
|
"The verdict, or the progress so far. Nothing is addressed by id here, because there is one working tree and so exactly one check.",
|
|
22
22
|
})
|
|
23
|
-
.output(
|
|
23
|
+
.output(CommandRunSchema),
|
|
24
24
|
run: oc
|
|
25
25
|
.route({
|
|
26
26
|
method: "POST",
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { oc } from "@orpc/contract";
|
|
2
|
+
import { SafetyLogEntrySchema, SafetyPolicySchema } from "../safety-policy.js";
|
|
3
|
+
import { OkSchema } from "../schemas/shared.js";
|
|
4
|
+
import { z } from "zod";
|
|
5
|
+
|
|
6
|
+
/* The owner's safety policy (.intentic/config/safety.md) and the log of what it decided.
|
|
7
|
+
*
|
|
8
|
+
* ITS OWN ROUTES RATHER THAN A FIELD ON /settings, and the split is the same one `rule-firings` already draws
|
|
9
|
+
* next door: the policy is a DOCUMENT edited at human speed, and the log is a value that changes on its own
|
|
10
|
+
* several times a turn. Folding either into the settings object would put a self-changing value inside the
|
|
11
|
+
* thing a screen optimistically patches, and would make every judged command a settings write.
|
|
12
|
+
*
|
|
13
|
+
* The policy is also the only config file here whose reader is a model rather than a parser, so it travels as
|
|
14
|
+
* text and is never parsed on the way through: whatever the owner wrote is what the judge reads.
|
|
15
|
+
*/
|
|
16
|
+
export const safetyContract = {
|
|
17
|
+
policy: oc
|
|
18
|
+
.route({
|
|
19
|
+
method: "GET",
|
|
20
|
+
path: "/safety/policy",
|
|
21
|
+
summary: "The safety policy this sandbox is judged against",
|
|
22
|
+
description:
|
|
23
|
+
"The document that decides when an agent stops to ask you before running something. Prose, not settings: it is read by the model that judges each command. When nobody has written one, this is the text the product ships with, and it describes the behaviour a fresh sandbox already has.",
|
|
24
|
+
})
|
|
25
|
+
.output(SafetyPolicySchema),
|
|
26
|
+
setPolicy: oc
|
|
27
|
+
.route({
|
|
28
|
+
method: "POST",
|
|
29
|
+
path: "/safety/policy",
|
|
30
|
+
summary: "Rewrite the safety policy",
|
|
31
|
+
description:
|
|
32
|
+
"Replaces the document whole. Nothing in it can widen what the sandbox is structurally allowed to do: it decides which of the things an agent may already do are worth interrupting you about.",
|
|
33
|
+
})
|
|
34
|
+
.input(z.object({ text: z.string().describe("The policy, as you want it written.") }))
|
|
35
|
+
.output(OkSchema),
|
|
36
|
+
// What the policy actually did, newest first. The half of the Safety page that makes the other half
|
|
37
|
+
// writable: nobody can author a policy for behaviour they cannot see, and a column of verdicts is what
|
|
38
|
+
// teaches an owner which line to add next.
|
|
39
|
+
log: oc
|
|
40
|
+
.route({
|
|
41
|
+
method: "GET",
|
|
42
|
+
path: "/safety/log",
|
|
43
|
+
summary: "Recent safety verdicts",
|
|
44
|
+
description:
|
|
45
|
+
"What was judged lately, what the judge decided, and whether you were interrupted. Newest first. This is where you find out why you were not asked about something, which is the question a policy page otherwise cannot answer.",
|
|
46
|
+
})
|
|
47
|
+
.output(z.array(SafetyLogEntrySchema)),
|
|
48
|
+
};
|
|
@@ -1,6 +1,15 @@
|
|
|
1
1
|
import { oc } from "@orpc/contract";
|
|
2
|
+
import { z } from "zod";
|
|
2
3
|
import { DayWindowQuerySchema, UsageRollupSchema } from "../schemas/usage.js";
|
|
3
4
|
|
|
5
|
+
// The one control over the headroom readings a person has: measure again, now, every connection. `force`
|
|
6
|
+
// ignores the freshness the daemon otherwise honours (a reading from the last minute is what the provider
|
|
7
|
+
// would answer again), because the person pressing it has just changed something about an account and is
|
|
8
|
+
// asking whether what they can see survived it.
|
|
9
|
+
export const RefreshPlanLimitsSchema = z.object({
|
|
10
|
+
force: z.boolean().default(false).describe("Measure again even if a reading was taken a moment ago."),
|
|
11
|
+
});
|
|
12
|
+
|
|
4
13
|
// The durable spend ledger (see UsageTurnSchema). Read-only over the wire, rows are appended daemon-side at
|
|
5
14
|
// turn end, so the ledger stays a trustworthy record of what was actually spent, the same principle as the
|
|
6
15
|
// activity log. `rollup` serves every cost/usage panel: it groups by day × provider × account × model, and the
|
|
@@ -16,4 +25,17 @@ export const usageContract = {
|
|
|
16
25
|
})
|
|
17
26
|
.input(DayWindowQuerySchema)
|
|
18
27
|
.output(UsageRollupSchema),
|
|
28
|
+
// Not the ledger's business, but the same tab: the plan-limit readings behind every ring, re-measured on
|
|
29
|
+
// request for every provider at once. The readings themselves arrive on /events as they land, and on the
|
|
30
|
+
// account lists afterwards; this answers once the sweep is done or its deadline passes.
|
|
31
|
+
refreshPlanLimits: oc
|
|
32
|
+
.route({
|
|
33
|
+
method: "POST",
|
|
34
|
+
path: "/usage/plan-limits/refresh",
|
|
35
|
+
summary: "Measure every account's plan limits again",
|
|
36
|
+
description:
|
|
37
|
+
"Reads how full each connected account's plan limits are, for every provider, and records it. Forced, it measures even accounts read a moment ago, which is the right thing when a plan was just changed and the question is whether the number on screen is still true.",
|
|
38
|
+
})
|
|
39
|
+
.input(RefreshPlanLimitsSchema)
|
|
40
|
+
.output(z.object({ ok: z.literal(true) })),
|
|
19
41
|
};
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { afterEach, expect, test, vi } from "vitest";
|
|
2
|
-
import { newConversationId } from "./conversation-ids.js";
|
|
2
|
+
import { CI_FIX_PREFIX, ciFixConversationId, newConversationId } from "./conversation-ids.js";
|
|
3
3
|
import { ConversationIdSchema } from "./schemas/agent.js";
|
|
4
4
|
|
|
5
5
|
const mockRandomValues = (values: readonly number[]) => {
|
|
@@ -59,3 +59,33 @@ test("ids are unique across a burst", () => {
|
|
|
59
59
|
const ids = new Set(Array.from({ length: 5_000 }, newConversationId));
|
|
60
60
|
expect(ids.size).toBe(5_000);
|
|
61
61
|
});
|
|
62
|
+
|
|
63
|
+
// The whole point of the derived id: the same failure names the same conversation, forever, from any browser.
|
|
64
|
+
test("a CI fix id is the same string every time it is derived", () => {
|
|
65
|
+
expect(ciFixConversationId(`web`, 4213)).toBe(`ci-fix-web-4213`);
|
|
66
|
+
expect(ciFixConversationId(`web`, 4213)).toBe(ciFixConversationId(`web`, 4213));
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
// A run id belongs to one forge project, not to the workspace: without the repo, two repos' run 42 would share
|
|
70
|
+
// an agent, and the second failure would be answered by the first one's conversation.
|
|
71
|
+
test("two repos with the same run number get different conversations", () => {
|
|
72
|
+
expect(ciFixConversationId(`web`, 42)).not.toBe(ciFixConversationId(`api`, 42));
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
// The id becomes a branch and a path, so the guard is not a matter of taste. Held over the repo names a
|
|
76
|
+
// workspace can actually produce: nested dirs, dots, spaces, punctuation, and something that slugs to nothing.
|
|
77
|
+
test("every derived id passes the conversation-id guard", () => {
|
|
78
|
+
const repos = [`root`, `web`, `apps/web`, `my repo`, `.dotted`, `UPPER_Case`, `___`, `x`.repeat(80)];
|
|
79
|
+
for (const repo of repos) {
|
|
80
|
+
for (const runId of [1, 42, 18_446_744_073]) {
|
|
81
|
+
const id = ciFixConversationId(repo, runId);
|
|
82
|
+
expect(ConversationIdSchema.safeParse(id).success).toBe(true);
|
|
83
|
+
expect(id.startsWith(CI_FIX_PREFIX)).toBe(true);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
// The join is a prefix scan over the fleet, so the prefix has to survive a repo name that starts with one.
|
|
89
|
+
test("the prefix is carried by every fix id", () => {
|
|
90
|
+
expect(ciFixConversationId(`ci-fix`, 7)).toBe(`ci-fix-ci-fix-7`);
|
|
91
|
+
});
|
package/src/conversation-ids.ts
CHANGED
|
@@ -25,9 +25,9 @@
|
|
|
25
25
|
* is not a matter of taste; the test beside it holds the whole generated space to it.
|
|
26
26
|
*
|
|
27
27
|
* Conversations the SANDBOX starts keep their own minters and their own prefixes (an automation's fire is
|
|
28
|
-
* `a-<automation>-<time>`, a CI fix is
|
|
29
|
-
* from, which beats a random pair for a card the user did not ask for. This is the default
|
|
30
|
-
* person opens.
|
|
28
|
+
* `a-<automation>-<time>`, a CI fix is DERIVED from the run it fixes, see below), those names say WHERE the
|
|
29
|
+
* conversation came from, which beats a random pair for a card the user did not ask for. This is the default
|
|
30
|
+
* for the ones a person opens.
|
|
31
31
|
*/
|
|
32
32
|
|
|
33
33
|
/* The two halves of the name. Kept short (nothing over seven letters) because the whole point is a string that
|
|
@@ -171,3 +171,37 @@ const tail = (): string => Array.from({ length: TAIL_LENGTH }, () => randomBelow
|
|
|
171
171
|
/* A fresh conversation id: `<adjective>-<noun>-<tail>`, e.g. `swift-otter-k9m2`. Sixteen characters or so
|
|
172
172
|
* against a UUID's thirty-six, and the first eleven of them are the ones a person reads. */
|
|
173
173
|
export const newConversationId = (): string => `${pick(ADJECTIVES)}-${pick(NOUNS)}-${tail()}`;
|
|
174
|
+
|
|
175
|
+
/* WHAT A CI FIX CONVERSATION IS CALLED, and the one id in this file that is DERIVED rather than drawn.
|
|
176
|
+
*
|
|
177
|
+
* It is derived because the question "has anybody already put an agent on this failure?" has to be answerable
|
|
178
|
+
* from the failure alone. Nothing records that anywhere: the fleet roster IS the record (the conversation, its
|
|
179
|
+
* worktree, its branch, its live status), and a roster is keyed by id, so an id nothing can re-compute is a
|
|
180
|
+
* record nothing can read. This one used to carry a timestamp and a per-process counter, which made every fix
|
|
181
|
+
* unfindable the moment the click was over: the board could offer "Fix with agent" and nothing else, forever,
|
|
182
|
+
* however many agents were already working on the row.
|
|
183
|
+
*
|
|
184
|
+
* Deriving it settles a second question the same way. One failed run is now one conversation, so a second press
|
|
185
|
+
* (from another tab, or from a reader who did not scroll to the state) CONTINUES the fix rather than starting a
|
|
186
|
+
* rival agent on a rival branch: the daemon refuses while the turn is live, and adds a turn to the same session
|
|
187
|
+
* once it is not. The old recipe made a fresh worktree every time.
|
|
188
|
+
*
|
|
189
|
+
* THE REPOSITORY IS IN IT because a run id belongs to one forge project, not to the workspace: two repos can
|
|
190
|
+
* each have a run 42, and without the repo their failures would share a conversation. It is slugified rather
|
|
191
|
+
* than hashed because this string is read by people, in `git branch` (`agent/ci-fix-web-4213`), in a path and
|
|
192
|
+
* on a card, and `ci-fix-a3f91c-4213` answers the join just as well while saying nothing. Two repositories
|
|
193
|
+
* whose names agree for the first REPO_SLUG_MAX characters share a slug, and would then share a fix
|
|
194
|
+
* conversation for identically-numbered runs, a pairing rare enough to prefer the readable name over guarding
|
|
195
|
+
* against it.
|
|
196
|
+
*/
|
|
197
|
+
export const CI_FIX_PREFIX = "ci-fix-";
|
|
198
|
+
// Leaves ~24 characters for the run id inside ConversationIdSchema's 64, which is twice the widest id either
|
|
199
|
+
// forge mints.
|
|
200
|
+
const REPO_SLUG_MAX = 32;
|
|
201
|
+
const repoSlug = (repo: string): string =>
|
|
202
|
+
repo
|
|
203
|
+
.toLowerCase()
|
|
204
|
+
.replace(/[^a-z0-9]+/g, "-")
|
|
205
|
+
.slice(0, REPO_SLUG_MAX)
|
|
206
|
+
.replace(/^-+|-+$/g, "") || "repo";
|
|
207
|
+
export const ciFixConversationId = (repo: string, runId: number): string => `${CI_FIX_PREFIX}${repoSlug(repo)}-${runId}`;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { describe, expect, test } from "vitest";
|
|
2
|
-
import { holdsCredentialMaterial } from "./credential-material.js";
|
|
2
|
+
import { holdsCredentialMaterial, maskCredentialMaterial } from "./credential-material.js";
|
|
3
3
|
|
|
4
4
|
/* The two directions are not symmetric and the tests are written to say so: a MISS here costs one permission
|
|
5
5
|
* card that should not have been raised, and a WRONG CLEAR un-gates a real credential read. So the "no" cases
|
|
@@ -118,3 +118,71 @@ describe("files that do not", () => {
|
|
|
118
118
|
expect(holdsCredentialMaterial("API=http://localhost:8080/v1\nSENTRY_DSN=https://abc123@o1.ingest.sentry.io/1\n")).toBe(false);
|
|
119
119
|
});
|
|
120
120
|
});
|
|
121
|
+
|
|
122
|
+
/* THE SAME TABLE APPLIED, and the property that matters is the one about what SURVIVES: this runs over the
|
|
123
|
+
* files an agent legitimately opens and edits, so a mask that takes the structure with it costs the work rather
|
|
124
|
+
* than protecting it. Every case below asserts both halves — the value is gone, the file is still readable. */
|
|
125
|
+
describe("masking what a credential file holds", () => {
|
|
126
|
+
test("a dotenv keeps its keys, its shape and its comments", () => {
|
|
127
|
+
const masked = maskCredentialMaterial(
|
|
128
|
+
["# staging", "PORT=3000", "NODE_ENV=production", 'STRIPE_SECRET="sk_live_51H8xQzRvKpLmNbTy"', "MAX_TOKENS=8192"].join("\n"),
|
|
129
|
+
);
|
|
130
|
+
expect(masked).toBe(["# staging", "PORT=3000", "NODE_ENV=production", 'STRIPE_SECRET="***"', "MAX_TOKENS=8192"].join("\n"));
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
/* THE ROUND TRIP THE WHOLE THING RESTS ON. A deferred value holds no credential and is the one thing a
|
|
134
|
+
* rewritten file must come back with intact: masking `{{secret:X}}` would have the model paste `***` over
|
|
135
|
+
* this workspace's own convention for a file that must not hold a credential. */
|
|
136
|
+
test("a placeholder and a secret reference come back untouched", () => {
|
|
137
|
+
const template = ["GITHUB_TOKEN=", "NPM_TOKEN=${NPM_TOKEN}", "API_KEY=<your-api-key>", "STRIPE_SECRET={{secret:STRIPE}}"].join("\n");
|
|
138
|
+
expect(maskCredentialMaterial(template)).toBe(template);
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
test("a private key keeps the lines that say what it is", () => {
|
|
142
|
+
const masked = maskCredentialMaterial("-----BEGIN OPENSSH PRIVATE KEY-----\nb3BlbnNzaC1rZXktdjEAAAAA\n-----END OPENSSH PRIVATE KEY-----\n");
|
|
143
|
+
expect(masked).toBe("-----BEGIN OPENSSH PRIVATE KEY-----\n***\n-----END OPENSSH PRIVATE KEY-----\n");
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
// A connection string with the host blanked is one nobody can debug, and the host is not the secret.
|
|
147
|
+
test("a url keeps everything but the password", () => {
|
|
148
|
+
expect(maskCredentialMaterial("DATABASE_URL=postgres://app:Rk29fPqz@db.internal:5432/app\n")).toBe(
|
|
149
|
+
"DATABASE_URL=postgres://app:***@db.internal:5432/app\n",
|
|
150
|
+
);
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
// No key names these, and none is needed: the issuer is in the token.
|
|
154
|
+
test("an issued token goes wherever it appears", () => {
|
|
155
|
+
expect(maskCredentialMaterial("gh auth: ghp_16C7e42F292c6912E7710c838347Ae178B4a expired")).toBe("gh auth: *** expired");
|
|
156
|
+
expect(maskCredentialMaterial("//registry.npmjs.org/:_authToken=npm_wCq3nTvR8xLm2ZbKp7HdJyE4sUaF6gN0iQ1t")).toBe(
|
|
157
|
+
"//registry.npmjs.org/:_authToken=***",
|
|
158
|
+
);
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
/* WHAT IT MUST NOT TOUCH, the other half of the bargain: this pass only ever runs over a result whose input
|
|
162
|
+
* named a credential file, and such a file is still mostly configuration the model has to read. */
|
|
163
|
+
test("the ordinary contents of a config file survive", () => {
|
|
164
|
+
for (const text of [
|
|
165
|
+
"registry=https://registry.npmjs.org/\nengine-strict=true\n",
|
|
166
|
+
"TOKEN_EXPIRY=3600\nMAX_TOKENS=8192\nREFRESH_TOKEN_URL=https://auth.example.com/refresh\n",
|
|
167
|
+
"Host github.com\n User git\n IdentityFile ~/.ssh/id_ed25519\n",
|
|
168
|
+
"ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIH1x radarsu@omen\n",
|
|
169
|
+
"POSTGRES_PASSWORD=dev\n",
|
|
170
|
+
]) {
|
|
171
|
+
expect(maskCredentialMaterial(text), text.slice(0, 30)).toBe(text);
|
|
172
|
+
}
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
// The point of the pass, stated as the property the gate now depends on: what comes back holds nothing.
|
|
176
|
+
test("what comes back no longer holds credential material", () => {
|
|
177
|
+
for (const text of [
|
|
178
|
+
"//registry.npmjs.org/:_authToken=npm_wCq3nTvR8xLm2ZbKp7HdJyE4sUaF6gN0iQ1t\n",
|
|
179
|
+
"[default]\naws_access_key_id = AKIAIOSFODNN7EXAMPLE\naws_secret_access_key = wJalrXUtnFEMI/K7MDENG\n",
|
|
180
|
+
'{"accessToken":"ya29.a0AfB_bJq2Lm","expiresAt":1767000000}',
|
|
181
|
+
"PORT=3000\nSTRIPE_SECRET=sk_live_51H8xQzRvKpLmNbTy\n",
|
|
182
|
+
"https://radarsu:ghp_S8kQ2mVx@github.com\n",
|
|
183
|
+
]) {
|
|
184
|
+
expect(holdsCredentialMaterial(text), `${text.slice(0, 30)} before`).toBe(true);
|
|
185
|
+
expect(holdsCredentialMaterial(maskCredentialMaterial(text)), `${text.slice(0, 30)} after`).toBe(false);
|
|
186
|
+
}
|
|
187
|
+
});
|
|
188
|
+
});
|