@intentic/sandbox-contract 1.239.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.
Files changed (310) hide show
  1. package/README.md +52 -16
  2. package/dist/agent-catalog.d.ts +5 -28
  3. package/dist/agent-catalog.d.ts.map +1 -1
  4. package/dist/agent-catalog.js +21 -157
  5. package/dist/agent-catalog.js.map +1 -1
  6. package/dist/agent-run-model.d.ts +3 -2
  7. package/dist/agent-run-model.d.ts.map +1 -1
  8. package/dist/agent-run-model.js +9 -6
  9. package/dist/agent-run-model.js.map +1 -1
  10. package/dist/agent-runtimes.d.ts +27 -0
  11. package/dist/agent-runtimes.d.ts.map +1 -0
  12. package/dist/agent-runtimes.js +113 -0
  13. package/dist/agent-runtimes.js.map +1 -0
  14. package/dist/approvals-execution.d.ts +11 -0
  15. package/dist/approvals-execution.d.ts.map +1 -0
  16. package/dist/approvals-execution.js +44 -0
  17. package/dist/approvals-execution.js.map +1 -0
  18. package/dist/arrival.d.ts +151 -0
  19. package/dist/arrival.d.ts.map +1 -0
  20. package/dist/arrival.js +56 -0
  21. package/dist/arrival.js.map +1 -0
  22. package/dist/card-status.d.ts +5 -0
  23. package/dist/card-status.d.ts.map +1 -0
  24. package/dist/card-status.js +74 -0
  25. package/dist/card-status.js.map +1 -0
  26. package/dist/chores/chores.d.ts.map +1 -1
  27. package/dist/chores/chores.js +1 -1
  28. package/dist/chores/chores.js.map +1 -1
  29. package/dist/command-classes.d.ts +0 -1
  30. package/dist/command-classes.d.ts.map +1 -1
  31. package/dist/command-classes.js +9 -2
  32. package/dist/command-classes.js.map +1 -1
  33. package/dist/command-run.d.ts +10 -0
  34. package/dist/command-run.d.ts.map +1 -0
  35. package/dist/command-run.js +46 -0
  36. package/dist/command-run.js.map +1 -0
  37. package/dist/contracts/agent.contract.d.ts +991 -771
  38. package/dist/contracts/agent.contract.d.ts.map +1 -1
  39. package/dist/contracts/agents.contract.d.ts +483 -1
  40. package/dist/contracts/agents.contract.d.ts.map +1 -1
  41. package/dist/contracts/agents.contract.js +20 -2
  42. package/dist/contracts/agents.contract.js.map +1 -1
  43. package/dist/contracts/approvals.contract.d.ts +99 -0
  44. package/dist/contracts/approvals.contract.d.ts.map +1 -0
  45. package/dist/contracts/approvals.contract.js +32 -0
  46. package/dist/contracts/approvals.contract.js.map +1 -0
  47. package/dist/contracts/ci.contract.d.ts +1 -4
  48. package/dist/contracts/ci.contract.d.ts.map +1 -1
  49. package/dist/contracts/ci.contract.js +1 -9
  50. package/dist/contracts/ci.contract.js.map +1 -1
  51. package/dist/contracts/claude.contract.d.ts +9 -0
  52. package/dist/contracts/claude.contract.d.ts.map +1 -1
  53. package/dist/contracts/cursor.contract.d.ts +6 -0
  54. package/dist/contracts/cursor.contract.d.ts.map +1 -1
  55. package/dist/contracts/git.contract.d.ts +63 -1
  56. package/dist/contracts/git.contract.d.ts.map +1 -1
  57. package/dist/contracts/git.contract.js +22 -4
  58. package/dist/contracts/git.contract.js.map +1 -1
  59. package/dist/contracts/grok.contract.d.ts +3 -0
  60. package/dist/contracts/grok.contract.d.ts.map +1 -1
  61. package/dist/contracts/keys.contract.d.ts +81 -0
  62. package/dist/contracts/keys.contract.d.ts.map +1 -0
  63. package/dist/contracts/keys.contract.js +51 -0
  64. package/dist/contracts/keys.contract.js.map +1 -0
  65. package/dist/contracts/panels.contract.d.ts +7 -0
  66. package/dist/contracts/panels.contract.d.ts.map +1 -1
  67. package/dist/contracts/prepush.contract.js +2 -2
  68. package/dist/contracts/providers.contract.d.ts +2 -0
  69. package/dist/contracts/providers.contract.d.ts.map +1 -1
  70. package/dist/contracts/runner.contract.d.ts +99 -101
  71. package/dist/contracts/runner.contract.d.ts.map +1 -1
  72. package/dist/contracts/safety.contract.d.ts +35 -0
  73. package/dist/contracts/safety.contract.d.ts.map +1 -0
  74. package/dist/contracts/safety.contract.js +32 -0
  75. package/dist/contracts/safety.contract.js.map +1 -0
  76. package/dist/contracts/sessions.contract.d.ts +211 -1
  77. package/dist/contracts/sessions.contract.d.ts.map +1 -1
  78. package/dist/contracts/settings.contract.d.ts +30 -32
  79. package/dist/contracts/settings.contract.d.ts.map +1 -1
  80. package/dist/contracts/system.contract.d.ts +277 -1
  81. package/dist/contracts/system.contract.d.ts.map +1 -1
  82. package/dist/contracts/translator.contract.d.ts +31 -3
  83. package/dist/contracts/translator.contract.d.ts.map +1 -1
  84. package/dist/contracts/usage.contract.d.ts +29 -20
  85. package/dist/contracts/usage.contract.d.ts.map +1 -1
  86. package/dist/contracts/usage.contract.js +13 -0
  87. package/dist/contracts/usage.contract.js.map +1 -1
  88. package/dist/contracts/workspace.contract.d.ts +8 -0
  89. package/dist/contracts/workspace.contract.d.ts.map +1 -1
  90. package/dist/conversation-ids.d.ts +2 -0
  91. package/dist/conversation-ids.d.ts.map +1 -1
  92. package/dist/conversation-ids.js +8 -0
  93. package/dist/conversation-ids.js.map +1 -1
  94. package/dist/credential-material.d.ts +1 -0
  95. package/dist/credential-material.d.ts.map +1 -1
  96. package/dist/credential-material.js +24 -2
  97. package/dist/credential-material.js.map +1 -1
  98. package/dist/definition.d.ts +51 -93
  99. package/dist/definition.d.ts.map +1 -1
  100. package/dist/definition.js +6 -28
  101. package/dist/definition.js.map +1 -1
  102. package/dist/events.d.ts +2947 -991
  103. package/dist/events.d.ts.map +1 -1
  104. package/dist/events.js +233 -82
  105. package/dist/events.js.map +1 -1
  106. package/dist/history-state.d.ts.map +1 -1
  107. package/dist/history-state.js +1 -0
  108. package/dist/history-state.js.map +1 -1
  109. package/dist/index.d.ts +2532 -1054
  110. package/dist/index.d.ts.map +1 -1
  111. package/dist/index.js +20 -5
  112. package/dist/index.js.map +1 -1
  113. package/dist/ingress-protocol.d.ts +13 -1
  114. package/dist/ingress-protocol.d.ts.map +1 -1
  115. package/dist/ingress-protocol.js +19 -0
  116. package/dist/ingress-protocol.js.map +1 -1
  117. package/dist/mentions.d.ts +3 -0
  118. package/dist/mentions.d.ts.map +1 -0
  119. package/dist/mentions.js +13 -0
  120. package/dist/mentions.js.map +1 -0
  121. package/dist/needs-action.d.ts +7 -0
  122. package/dist/needs-action.d.ts.map +1 -0
  123. package/dist/needs-action.js +3 -0
  124. package/dist/needs-action.js.map +1 -0
  125. package/dist/overlay-lint.d.ts +7 -0
  126. package/dist/overlay-lint.d.ts.map +1 -0
  127. package/dist/overlay-lint.js +68 -0
  128. package/dist/overlay-lint.js.map +1 -0
  129. package/dist/owner-ticket.d.ts +18 -0
  130. package/dist/owner-ticket.d.ts.map +1 -0
  131. package/dist/owner-ticket.js +51 -0
  132. package/dist/owner-ticket.js.map +1 -0
  133. package/dist/plan-pools.d.ts +12 -0
  134. package/dist/plan-pools.d.ts.map +1 -0
  135. package/dist/plan-pools.js +30 -0
  136. package/dist/plan-pools.js.map +1 -0
  137. package/dist/provider-specs.d.ts +228 -0
  138. package/dist/provider-specs.d.ts.map +1 -0
  139. package/dist/provider-specs.js +124 -0
  140. package/dist/provider-specs.js.map +1 -0
  141. package/dist/quick-model.d.ts.map +1 -1
  142. package/dist/quick-model.js +2 -1
  143. package/dist/quick-model.js.map +1 -1
  144. package/dist/runner-protocol.js +2 -2
  145. package/dist/runner-protocol.js.map +1 -1
  146. package/dist/runtime-state.d.ts +2 -2
  147. package/dist/runtime-state.d.ts.map +1 -1
  148. package/dist/runtime-state.js +1 -1
  149. package/dist/runtime-state.js.map +1 -1
  150. package/dist/safety-policy.d.ts +49 -0
  151. package/dist/safety-policy.d.ts.map +1 -0
  152. package/dist/safety-policy.js +52 -0
  153. package/dist/safety-policy.js.map +1 -0
  154. package/dist/schemas/agent.d.ts +21 -3
  155. package/dist/schemas/agent.d.ts.map +1 -1
  156. package/dist/schemas/agent.js +18 -8
  157. package/dist/schemas/agent.js.map +1 -1
  158. package/dist/schemas/agents.d.ts +19 -0
  159. package/dist/schemas/agents.d.ts.map +1 -1
  160. package/dist/schemas/agents.js +15 -0
  161. package/dist/schemas/agents.js.map +1 -1
  162. package/dist/schemas/approvals.d.ts +241 -0
  163. package/dist/schemas/approvals.d.ts.map +1 -0
  164. package/dist/schemas/approvals.js +63 -0
  165. package/dist/schemas/approvals.js.map +1 -0
  166. package/dist/schemas/automations.d.ts +5 -0
  167. package/dist/schemas/automations.d.ts.map +1 -1
  168. package/dist/schemas/ci.d.ts +5 -9
  169. package/dist/schemas/ci.d.ts.map +1 -1
  170. package/dist/schemas/ci.js +3 -10
  171. package/dist/schemas/ci.js.map +1 -1
  172. package/dist/schemas/computers.d.ts +31 -5
  173. package/dist/schemas/computers.d.ts.map +1 -1
  174. package/dist/schemas/computers.js +7 -5
  175. package/dist/schemas/computers.js.map +1 -1
  176. package/dist/schemas/environment.d.ts +11 -131
  177. package/dist/schemas/environment.d.ts.map +1 -1
  178. package/dist/schemas/environment.js +5 -42
  179. package/dist/schemas/environment.js.map +1 -1
  180. package/dist/schemas/git-history.d.ts +4 -0
  181. package/dist/schemas/git-history.d.ts.map +1 -1
  182. package/dist/schemas/git.d.ts +179 -0
  183. package/dist/schemas/git.d.ts.map +1 -1
  184. package/dist/schemas/git.js +39 -1
  185. package/dist/schemas/git.js.map +1 -1
  186. package/dist/schemas/panels.d.ts +21 -0
  187. package/dist/schemas/panels.d.ts.map +1 -1
  188. package/dist/schemas/panels.js +5 -0
  189. package/dist/schemas/panels.js.map +1 -1
  190. package/dist/schemas/plan-limits.d.ts +50 -3
  191. package/dist/schemas/plan-limits.d.ts.map +1 -1
  192. package/dist/schemas/plan-limits.js +11 -6
  193. package/dist/schemas/plan-limits.js.map +1 -1
  194. package/dist/schemas/provider-oauth.d.ts +6 -0
  195. package/dist/schemas/provider-oauth.d.ts.map +1 -1
  196. package/dist/schemas/provider-subscriptions.d.ts +2 -1
  197. package/dist/schemas/provider-subscriptions.d.ts.map +1 -1
  198. package/dist/schemas/provider-subscriptions.js +2 -1
  199. package/dist/schemas/provider-subscriptions.js.map +1 -1
  200. package/dist/schemas/settings.d.ts +29 -16
  201. package/dist/schemas/settings.d.ts.map +1 -1
  202. package/dist/schemas/settings.js +13 -20
  203. package/dist/schemas/settings.js.map +1 -1
  204. package/dist/schemas/system.d.ts +3 -0
  205. package/dist/schemas/system.d.ts.map +1 -1
  206. package/dist/schemas/system.js +5 -1
  207. package/dist/schemas/system.js.map +1 -1
  208. package/dist/schemas/workspace-repos.d.ts +14 -0
  209. package/dist/schemas/workspace-repos.d.ts.map +1 -1
  210. package/dist/schemas/workspace-repos.js +5 -0
  211. package/dist/schemas/workspace-repos.js.map +1 -1
  212. package/dist/schemas/workspace-tree.d.ts +1 -0
  213. package/dist/schemas/workspace-tree.d.ts.map +1 -1
  214. package/dist/schemas/workspace-tree.js +3 -0
  215. package/dist/schemas/workspace-tree.js.map +1 -1
  216. package/dist/transcript-fold.d.ts +31 -0
  217. package/dist/transcript-fold.d.ts.map +1 -0
  218. package/dist/transcript-fold.js +457 -0
  219. package/dist/transcript-fold.js.map +1 -0
  220. package/dist/workspace-state.d.ts +20 -3
  221. package/dist/workspace-state.d.ts.map +1 -1
  222. package/dist/workspace-state.js +22 -2
  223. package/dist/workspace-state.js.map +1 -1
  224. package/package.json +27 -5
  225. package/src/agent-catalog.test.ts +1 -1
  226. package/src/agent-catalog.ts +76 -555
  227. package/src/agent-run-model.test.ts +31 -15
  228. package/src/agent-run-model.ts +26 -12
  229. package/src/agent-runtimes.ts +419 -0
  230. package/src/approvals-execution.ts +96 -0
  231. package/src/arrival.ts +160 -0
  232. package/src/capability-ledger.test.ts +6 -4
  233. package/src/card-status.ts +93 -0
  234. package/src/chores/chores.ts +6 -3
  235. package/src/command-classes.test.ts +25 -0
  236. package/src/command-classes.ts +47 -5
  237. package/src/command-run.ts +78 -0
  238. package/src/contract-lock.test.ts +1 -1
  239. package/src/contract-lock.ts +2 -2
  240. package/src/contracts/agents.contract.ts +35 -2
  241. package/src/contracts/approvals.contract.ts +37 -0
  242. package/src/contracts/ci.contract.ts +0 -13
  243. package/src/contracts/git.contract.ts +28 -3
  244. package/src/contracts/keys.contract.ts +79 -0
  245. package/src/contracts/prepush.contract.ts +3 -3
  246. package/src/contracts/safety.contract.ts +48 -0
  247. package/src/contracts/usage.contract.ts +22 -0
  248. package/src/conversation-ids.test.ts +31 -1
  249. package/src/conversation-ids.ts +37 -3
  250. package/src/credential-material.test.ts +69 -1
  251. package/src/credential-material.ts +86 -5
  252. package/src/definition.ts +25 -53
  253. package/src/events.ts +508 -198
  254. package/src/history-state.ts +6 -0
  255. package/src/index.ts +24 -5
  256. package/src/ingress-protocol.ts +52 -1
  257. package/src/mentions.test.ts +22 -0
  258. package/src/mentions.ts +25 -0
  259. package/src/needs-action.ts +14 -0
  260. package/src/overlay-lint.test.ts +89 -0
  261. package/src/overlay-lint.ts +116 -0
  262. package/src/owner-ticket.test.ts +54 -0
  263. package/src/owner-ticket.ts +102 -0
  264. package/src/plan-pools.test.ts +71 -0
  265. package/src/plan-pools.ts +82 -0
  266. package/src/provider-specs.test.ts +130 -0
  267. package/src/provider-specs.ts +352 -0
  268. package/src/quick-model.ts +9 -5
  269. package/src/runner-protocol.ts +2 -2
  270. package/src/runtime-state.ts +6 -6
  271. package/src/safety-policy.ts +165 -0
  272. package/src/schemas/agent.ts +62 -17
  273. package/src/schemas/agents.ts +71 -0
  274. package/src/schemas/approvals.ts +174 -0
  275. package/src/schemas/ci.ts +19 -26
  276. package/src/schemas/computers.ts +52 -11
  277. package/src/schemas/environment.ts +21 -100
  278. package/src/schemas/git.ts +121 -6
  279. package/src/schemas/issues.ts +1 -1
  280. package/src/schemas/panels.ts +20 -0
  281. package/src/schemas/plan-limits.ts +58 -11
  282. package/src/schemas/provider-subscriptions.ts +7 -2
  283. package/src/schemas/settings.ts +67 -93
  284. package/src/schemas/system.ts +12 -1
  285. package/src/schemas/workspace-repos.ts +9 -0
  286. package/src/schemas/workspace-tree.ts +11 -0
  287. package/src/transcript-fold.test.ts +493 -0
  288. package/src/transcript-fold.ts +655 -0
  289. package/src/workspace-state.test.ts +77 -29
  290. package/src/workspace-state.ts +99 -31
  291. package/dist/contracts/drafts.contract.d.ts +0 -57
  292. package/dist/contracts/drafts.contract.d.ts.map +0 -1
  293. package/dist/contracts/drafts.contract.js +0 -32
  294. package/dist/contracts/drafts.contract.js.map +0 -1
  295. package/dist/definition-action.d.ts +0 -7
  296. package/dist/definition-action.d.ts.map +0 -1
  297. package/dist/definition-action.js +0 -3
  298. package/dist/definition-action.js.map +0 -1
  299. package/dist/publish-drafts.d.ts +0 -7
  300. package/dist/publish-drafts.d.ts.map +0 -1
  301. package/dist/publish-drafts.js +0 -20
  302. package/dist/publish-drafts.js.map +0 -1
  303. package/dist/schemas/drafts.d.ts +0 -84
  304. package/dist/schemas/drafts.d.ts.map +0 -1
  305. package/dist/schemas/drafts.js +0 -44
  306. package/dist/schemas/drafts.js.map +0 -1
  307. package/src/contracts/drafts.contract.ts +0 -35
  308. package/src/definition-action.ts +0 -8
  309. package/src/publish-drafts.ts +0 -59
  310. 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 (RestoredMessageSchema); the
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: "Send commits to the remote",
465
+ summary: "Start sending commits to the remote",
460
466
  description:
461
- "Pushes the current branch, setting its upstream on first push. A rejected push, a missing remote and missing credentials all come back as reported outcomes rather than failures.",
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(GitActionResultSchema),
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 { PrepushRunSchema } from "../schemas/ci.js";
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 PrepushRunSchema for where this sits and why). Three verbs about ONE run: the check answers about the
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(PrepushRunSchema),
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
+ });
@@ -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 minted from the run), those names say WHERE the conversation came
29
- * from, which beats a random pair for a card the user did not ask for. This is the default for the ones a
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
+ });