toolroll 0.9.67 → 0.9.69
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/CHANGELOG.md +70 -0
- package/dist/acp-client.js +1 -3
- package/dist/agent-board.d.ts +0 -1
- package/dist/agent-board.js +0 -1
- package/dist/agent-fence.d.ts +58 -0
- package/dist/agent-fence.js +214 -19
- package/dist/agentconfig.d.ts +1 -1
- package/dist/agentconfig.js +6 -2
- package/dist/batch-checks.js +14 -26
- package/dist/browser/workspace.css +1 -1
- package/dist/browser/workspace.js +31 -29
- package/dist/builder.d.ts +9 -0
- package/dist/builder.js +30 -27
- package/dist/canonical-json.d.ts +14 -0
- package/dist/canonical-json.js +36 -0
- package/dist/chat-actions.js +1 -2
- package/dist/chat-admission.d.ts +19 -0
- package/dist/chat-admission.js +22 -0
- package/dist/chat-channel.js +1 -1
- package/dist/chat-delivery.js +0 -1
- package/dist/chat-follow.d.ts +17 -0
- package/dist/chat-follow.js +32 -0
- package/dist/chat-messages.js +0 -1
- package/dist/chat-settings-page.d.ts +23 -0
- package/dist/chat-settings-page.js +41 -0
- package/dist/check-access.d.ts +54 -0
- package/dist/check-access.js +80 -0
- package/dist/claim.js +3 -6
- package/dist/claude-print.d.ts +12 -0
- package/dist/claude-print.js +34 -0
- package/dist/cli-http.d.ts +0 -7
- package/dist/cli-http.js +0 -21
- package/dist/cli.d.ts +3 -10
- package/dist/cli.js +15 -24
- package/dist/coding-provider.js +2 -1
- package/dist/coding-workspace.js +1 -1
- package/dist/{team-cli.d.ts → connect-cli.d.ts} +5 -9
- package/dist/connect-cli.js +331 -0
- package/dist/containment.d.ts +0 -2
- package/dist/containment.js +2 -6
- package/dist/contracts/chat-actions.d.ts +0 -1
- package/dist/contracts/chat-content.d.ts +0 -4
- package/dist/contracts/chat-content.js +0 -2
- package/dist/contracts/chat-tables.d.ts +0 -1
- package/dist/contracts/cli.d.ts +0 -62
- package/dist/contracts/cli.js +0 -4
- package/dist/contracts/coding-workspace.js +1 -1
- package/dist/contracts/console-api.d.ts +1 -0
- package/dist/contracts/console-api.js +1 -0
- package/dist/contracts/contract.js +1 -1
- package/dist/contracts/flow-card.d.ts +0 -1
- package/dist/contracts/flow-propose.d.ts +0 -1
- package/dist/contracts/flow-send.js +1 -1
- package/dist/contracts/gateway-tools.d.ts +0 -1
- package/dist/contracts/integration-metadata.js +1 -1
- package/dist/contracts/lead-tools.d.ts +0 -1
- package/dist/contracts/memory-pass.d.ts +0 -1
- package/dist/contracts/memory-pass.js +1 -1
- package/dist/contracts/plan.js +1 -1
- package/dist/contracts/project-knowledge.js +1 -1
- package/dist/contracts/project-skills.d.ts +0 -1
- package/dist/contracts/proof.js +1 -1
- package/dist/contracts/recipes.js +1 -1
- package/dist/contracts/scope.js +1 -1
- package/dist/contracts/scout-report.js +1 -1
- package/dist/contracts/teams-callback.d.ts +0 -1
- package/dist/converse.d.ts +0 -6
- package/dist/converse.js +0 -19
- package/dist/daemon.js +3 -2
- package/dist/decision.d.ts +1 -3
- package/dist/deprecations.d.ts +12 -4
- package/dist/deprecations.js +27 -4
- package/dist/desktop-update-recovery.js +1 -1
- package/dist/desktop-update.js +3 -2
- package/dist/discord-chat.js +4 -15
- package/dist/discord-settings.d.ts +2 -6
- package/dist/discord-settings.js +10 -38
- package/dist/discord.js +12 -23
- package/dist/evidence.d.ts +0 -1
- package/dist/evidence.js +0 -3
- package/dist/exec.d.ts +0 -9
- package/dist/exec.js +3 -81
- package/dist/flow-actions.js +0 -3
- package/dist/flow-draft.d.ts +1 -3
- package/dist/flow-draft.js +15 -28
- package/dist/flow-scripts.d.ts +1 -8
- package/dist/flow-scripts.js +0 -13
- package/dist/flow-send.js +1 -1
- package/dist/flow-share.d.ts +0 -2
- package/dist/flow-share.js +1 -6
- package/dist/flow-steps.js +1 -1
- package/dist/flow-triggers.d.ts +0 -1
- package/dist/flow-triggers.js +1 -2
- package/dist/flow-unattended.js +1 -12
- package/dist/flows.js +1 -1
- package/dist/health-cli.d.ts +2 -2
- package/dist/health-cli.js +1 -1
- package/dist/invoke.d.ts +39 -5
- package/dist/invoke.js +181 -24
- package/dist/is-record.d.ts +2 -0
- package/dist/is-record.js +2 -0
- package/dist/lead-doors.d.ts +1 -5
- package/dist/lead-doors.js +0 -4
- package/dist/lead-tools.js +3 -6
- package/dist/live-detail.d.ts +0 -1
- package/dist/live-detail.js +0 -6
- package/dist/live-terminal.js +2 -1
- package/dist/live.d.ts +0 -1
- package/dist/live.js +1 -2
- package/dist/mcp-core.js +0 -2
- package/dist/memory-pass.js +1 -1
- package/dist/model-catalog.js +1 -1
- package/dist/modes.js +1 -12
- package/dist/operate.d.ts +4 -0
- package/dist/operate.js +456 -750
- package/dist/password-bearer.d.ts +0 -2
- package/dist/password-bearer.js +0 -4
- package/dist/peek-cli.js +2 -1
- package/dist/peek.d.ts +2 -26
- package/dist/peek.js +2 -168
- package/dist/phase-routing.d.ts +0 -4
- package/dist/phase-routing.js +2 -21
- package/dist/planner-source.d.ts +2 -1
- package/dist/planner-source.js +2 -12
- package/dist/planner.js +2 -14
- package/dist/process-tree.d.ts +0 -2
- package/dist/process-tree.js +0 -10
- package/dist/project-delete-ui.d.ts +16 -0
- package/dist/project-delete-ui.js +60 -1
- package/dist/project-delete.d.ts +0 -2
- package/dist/project-delete.js +1 -3
- package/dist/project-learning.d.ts +0 -14
- package/dist/project-skills.d.ts +0 -1
- package/dist/project-skills.js +0 -14
- package/dist/project-tools.js +2 -10
- package/dist/provider.d.ts +0 -17
- package/dist/provider.js +0 -26
- package/dist/pulls.d.ts +0 -2
- package/dist/pulls.js +0 -9
- package/dist/release-adapters.d.ts +3 -0
- package/dist/release-adapters.js +15 -5
- package/dist/remote-exec.d.ts +3 -2
- package/dist/remote-exec.js +3 -2
- package/dist/repos.d.ts +0 -5
- package/dist/repos.js +2 -10
- package/dist/repository-context.d.ts +0 -2
- package/dist/repository-context.js +0 -4
- package/dist/restart-certification.d.ts +0 -2
- package/dist/restart-certification.js +1 -5
- package/dist/result-follow-ups.d.ts +1 -4
- package/dist/result-follow-ups.js +11 -21
- package/dist/result-review.d.ts +0 -2
- package/dist/result-review.js +0 -2
- package/dist/retention.d.ts +0 -1
- package/dist/retention.js +0 -1
- package/dist/review-context.d.ts +0 -1
- package/dist/review-context.js +1 -12
- package/dist/reviewer.d.ts +0 -16
- package/dist/reviewer.js +0 -18
- package/dist/run-closeout.js +2 -1
- package/dist/scope.d.ts +9 -0
- package/dist/scope.js +13 -18
- package/dist/scout-browser.js +2 -1
- package/dist/serve.js +11 -2
- package/dist/server/console.css +5 -133
- package/dist/server/flows.js +2 -2
- package/dist/server/http.d.ts +4 -0
- package/dist/server/http.js +5 -0
- package/dist/server/pages.js +2 -2
- package/dist/server/people-tokens.js +3 -4
- package/dist/server/remote.js +1 -6
- package/dist/server/render-chat.d.ts +9 -2
- package/dist/server/render-chat.js +53 -7
- package/dist/server/render-results.d.ts +3 -6
- package/dist/server/render-results.js +4 -9
- package/dist/server/render-tasks.js +6 -6
- package/dist/server/route-table.d.ts +0 -4
- package/dist/server/route-table.js +1 -6
- package/dist/server/settings.js +60 -23
- package/dist/server/task-views.js +1 -1
- package/dist/server/tasks.js +5 -3
- package/dist/setup-guide.d.ts +2 -2
- package/dist/skills.d.ts +0 -6
- package/dist/skills.js +0 -8
- package/dist/slack-chat.d.ts +1 -7
- package/dist/slack-chat.js +5 -18
- package/dist/slack-settings.d.ts +2 -6
- package/dist/slack-settings.js +10 -38
- package/dist/slack.js +12 -23
- package/dist/sleep.d.ts +2 -0
- package/dist/sleep.js +2 -0
- package/dist/store.d.ts +29 -75
- package/dist/store.js +89 -180
- package/dist/structured-output.d.ts +0 -1
- package/dist/structured-output.js +0 -1
- package/dist/subagent-work.d.ts +0 -2
- package/dist/subagent-work.js +0 -2
- package/dist/subagents.d.ts +1 -3
- package/dist/subagents.js +20 -33
- package/dist/subscription-chat.d.ts +1 -3
- package/dist/subscription-chat.js +2 -3
- package/dist/surface.js +2 -1
- package/dist/task-sizing.d.ts +1 -4
- package/dist/task-sizing.js +3 -10
- package/dist/teams-chat.js +4 -11
- package/dist/teams.js +12 -19
- package/dist/telegram-lead.js +2 -2
- package/dist/telegram.js +1 -1
- package/dist/token-notices.d.ts +0 -11
- package/dist/token-notices.js +1 -5
- package/dist/tool-secret-store.d.ts +0 -3
- package/dist/tool-secret-store.js +2 -11
- package/dist/toolroll-update.d.ts +0 -2
- package/dist/toolroll-update.js +2 -2
- package/dist/verification-evidence.d.ts +0 -1
- package/dist/verification-evidence.js +0 -1
- package/dist/webhooks.d.ts +0 -1
- package/dist/webhooks.js +0 -1
- package/dist/workspace-ui.d.ts +0 -8
- package/dist/workspace-ui.js +1 -6
- package/package.json +1 -1
- package/dist/contracts/native-census.d.ts +0 -42
- package/dist/contracts/native-census.js +0 -36
- package/dist/process-recovery-anchor.d.ts +0 -75
- package/dist/process-recovery-anchor.js +0 -188
- package/dist/process-recovery-coalition.d.ts +0 -25
- package/dist/process-recovery-coalition.js +0 -105
- package/dist/process-recovery-eligibility.d.ts +0 -179
- package/dist/process-recovery-eligibility.js +0 -174
- package/dist/process-recovery-native.d.ts +0 -161
- package/dist/process-recovery-native.js +0 -595
- package/dist/process-recovery-provenance.d.ts +0 -163
- package/dist/process-recovery-provenance.js +0 -292
- package/dist/process-recovery-services.d.ts +0 -56
- package/dist/process-recovery-services.js +0 -191
- package/dist/process-recovery-settlement.d.ts +0 -32
- package/dist/process-recovery-settlement.js +0 -133
- package/dist/process-recovery.d.ts +0 -41
- package/dist/process-recovery.js +0 -91
- package/dist/session-brief.d.ts +0 -8
- package/dist/session-brief.js +0 -24
- package/dist/session-cli.d.ts +0 -42
- package/dist/session-cli.js +0 -271
- package/dist/session-contract.d.ts +0 -183
- package/dist/session-contract.js +0 -122
- package/dist/session-remote.d.ts +0 -19
- package/dist/session-remote.js +0 -86
- package/dist/session-service.d.ts +0 -22
- package/dist/session-service.js +0 -157
- package/dist/team-cli.js +0 -692
package/dist/builder.d.ts
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { type NetworkProfile } from "./egress.js";
|
|
2
|
+
import { type CheckAccess } from "./check-access.js";
|
|
1
3
|
import type { WritesMode } from "./writes-mode.js";
|
|
2
4
|
import type { LaunchNetwork } from "./invoke.js";
|
|
3
5
|
import type { ProviderId } from "./contracts/provider.js";
|
|
@@ -187,6 +189,13 @@ export declare function redactSecretAssignments(text: string): string;
|
|
|
187
189
|
/** A build's network (egress.ts): the one its approval sealed, or — sealed before network policies existed — the
|
|
188
190
|
* narrowed legacy one (provider hosts and the repository's registries, nothing added since). */
|
|
189
191
|
export declare function launchNetworkOf(store: Store, taskRef: number, profile: ExecutionProfile): LaunchNetwork;
|
|
192
|
+
/** A build's checks' level and network: its approval's level as this platform can give it (sign-ins sealed before
|
|
193
|
+
* run sandboxed while sign-ins are off, and the run is stamped with that, what actually ran), and under sign-ins the network its
|
|
194
|
+
* approval sealed less its agent's own provider hosts (never its checks'). */
|
|
195
|
+
export declare function checkLaunchOf(store: Store, runId: number, profile: ExecutionProfile, platform?: NodeJS.Platform): {
|
|
196
|
+
access: CheckAccess;
|
|
197
|
+
network: NetworkProfile | null;
|
|
198
|
+
};
|
|
190
199
|
/** Where a build may write (writes-mode.ts): what its approval sealed, or — sealed before write allowlists — anywhere
|
|
191
200
|
* its fence allows, as it was approved. */
|
|
192
201
|
export declare function launchWritesOf(profile: ExecutionProfile): WritesMode;
|
package/dist/builder.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { legacyNetwork, repositoryEcosystems } from "./egress.js";
|
|
2
|
+
import { CHECK_ACCESS_DEFAULT, checkAccessHere, checkNetworkOf } from "./check-access.js";
|
|
2
3
|
import { checkoutGit } from "./teardown-git.js";
|
|
3
4
|
import { isAcpProvider } from "./acp-agents.js";
|
|
4
5
|
import { OBSERVATION_MAILBOX, observationBrief, parseObservationCases, collectObservations } from "./observations.js";
|
|
@@ -74,7 +75,7 @@ import { captureParkEvidence, captureTerminalDiff, captureBaseTree, evidenceRoot
|
|
|
74
75
|
import { attemptKeyOf, closeRunQuestions, noteOpenTool, recordAgentTurn } from "./agent-board.js";
|
|
75
76
|
import { openStatusFile, pulseAgentBoard, removeStatusFile, statusBrief } from "./agent-pulse.js";
|
|
76
77
|
import { withAgentTurns } from "./agent-turns.js";
|
|
77
|
-
import { PROOF_LIMITS, parseProof, serializeProof, adjudicate,
|
|
78
|
+
import { PROOF_LIMITS, parseProof, serializeProof, adjudicate, sameDiffStatFacts } from "./proof.js";
|
|
78
79
|
import { parseHandoffArtifact } from "./contracts/handoff.js";
|
|
79
80
|
import { captureReviewContext } from "./review-context.js";
|
|
80
81
|
import { authoritySnapshotDigest, classifyRevisionAuthority, isMilestoneRegression, milestonesOf, parseExecutionPlanDocument, parsePlanRevisionProposal, parseProgressSnapshot, renderExecutionPlanDocument, PROGRESS_LIMITS, REVISION_LIMITS, } from "./plan.js";
|
|
@@ -268,7 +269,26 @@ function buildCheckoutGit(given, worktree, mainCheckout = null) {
|
|
|
268
269
|
/** A run's setup and check commands, in its build's fence, writing where the approval this build runs under sealed
|
|
269
270
|
* (never the scope as it may since have been rewritten; invoke.ts fencedCommands). */
|
|
270
271
|
function commandRunner(store, runId, profile, inner) {
|
|
271
|
-
|
|
272
|
+
// How far its checks reach (check-access.ts): what its approval sealed, stamped on the run (again: the dispatch stamped
|
|
273
|
+
// it first) so a later check of its result never runs wider; under sign-ins, behind its checks' own network.
|
|
274
|
+
const { access, network } = checkLaunchOf(store, runId, profile);
|
|
275
|
+
store.stampRunCheckAccess(runId, access, network);
|
|
276
|
+
return fencedCommands(store, runId, launchWritesOf(profile), inner, undefined, access, network ?? undefined);
|
|
277
|
+
}
|
|
278
|
+
/** A build's checks' level and network: its approval's level as this platform can give it (sign-ins sealed before
|
|
279
|
+
* run sandboxed while sign-ins are off, and the run is stamped with that, what actually ran), and under sign-ins the network its
|
|
280
|
+
* approval sealed less its agent's own provider hosts (never its checks'). */
|
|
281
|
+
export function checkLaunchOf(store, runId, profile, platform = process.platform) {
|
|
282
|
+
const access = checkAccessHere(profile.checkAccess ?? CHECK_ACCESS_DEFAULT, platform);
|
|
283
|
+
if (access !== "sign-ins")
|
|
284
|
+
return { access, network: null };
|
|
285
|
+
const taskRef = store.getRun(runId)?.taskRef;
|
|
286
|
+
const sealed = taskRef === undefined ? undefined : launchNetworkOf(store, taskRef, profile);
|
|
287
|
+
// No sealed network to narrow from: its checks run sandboxed.
|
|
288
|
+
if (sealed === undefined || !("policy" in sealed))
|
|
289
|
+
return { access: CHECK_ACCESS_DEFAULT, network: null };
|
|
290
|
+
const repo = store.refForId(taskRef)?.repo ?? null;
|
|
291
|
+
return { access, network: checkNetworkOf(sealed.policy, profile.provider, repo === null ? { mode: "allowlist", hosts: [], loopback: "proxy-only" } : store.projectNetwork(repo)) };
|
|
272
292
|
}
|
|
273
293
|
/** Where a build may write (writes-mode.ts): what its approval sealed, or — sealed before write allowlists — anywhere
|
|
274
294
|
* its fence allows, as it was approved. */
|
|
@@ -369,7 +389,7 @@ export function proveApprovedProfile(scope, given) {
|
|
|
369
389
|
* that forgot one is exactly the caller this is protecting against.
|
|
370
390
|
*/
|
|
371
391
|
export async function build(store, request) {
|
|
372
|
-
const { taskId, taskRef, runner, worktree, branch, now, permissionMode = "auto", skipPermissions = false, provider = "claude",
|
|
392
|
+
const { taskId, taskRef, runner, worktree, branch, now, permissionMode = "auto", skipPermissions = false, provider = "claude", maxTurns = DEFAULT_MAX_TURNS, timeoutMs = DEFAULT_BUILD_TIMEOUT_MS, agent, git: givenGit = run, } = request;
|
|
373
393
|
// The plane's own git on this build's checkout runs hardened, and never where the checkout's config names a command.
|
|
374
394
|
const git = buildCheckoutGit(givenGit, worktree, store.getWorktree(worktree)?.repo ?? null);
|
|
375
395
|
const scope = store.getScope(taskId);
|
|
@@ -422,6 +442,12 @@ export async function build(store, request) {
|
|
|
422
442
|
// resume below can match on them honestly.
|
|
423
443
|
const provenScopeDigest = scope?.approvedDigest ?? "";
|
|
424
444
|
const provenProfileDigest = profileDigestOf(effective.profile);
|
|
445
|
+
// Its checks' level and network, stamped now with the proof: a batched result that runs no command here still
|
|
446
|
+
// carries what its approval allows its checks (check-access.ts).
|
|
447
|
+
{
|
|
448
|
+
const launch = checkLaunchOf(store, request.runId, effective.profile);
|
|
449
|
+
store.stampRunCheckAccess(request.runId, launch.access, launch.network);
|
|
450
|
+
}
|
|
425
451
|
store.stampRun(request.runId, {
|
|
426
452
|
scopeDigest: provenScopeDigest,
|
|
427
453
|
profileDigest: provenProfileDigest,
|
|
@@ -1527,7 +1553,7 @@ export async function settleProviderOutcome(captured, result) {
|
|
|
1527
1553
|
}
|
|
1528
1554
|
catch { /* The commit gate still excludes it. */ }
|
|
1529
1555
|
}
|
|
1530
|
-
const { store, request, agent, git, worktree, branch, baseRevision, taskId, taskRef, runner, provider, scope, effective, answers, timeoutMs, root, mailbox, done,
|
|
1556
|
+
const { store, request, agent, git, worktree, branch, baseRevision, taskId, taskRef, runner, provider, scope, effective, answers, timeoutMs, root, mailbox, done, clock } = captured;
|
|
1531
1557
|
// THE STOP FENCE after the provider (v52): a stop recorded while the
|
|
1532
1558
|
// agent ran ends the attempt HERE, before any handoff is read, any park
|
|
1533
1559
|
// sealed, or any commit made — whatever the process wrote is preserved
|
|
@@ -3534,29 +3560,6 @@ async function stripEvidenceImages(git, worktree) {
|
|
|
3534
3560
|
const rest = await git(GIT, ["--no-optional-locks", "diff", "--cached", "--quiet"], { cwd: worktree });
|
|
3535
3561
|
return { ok: true, notice: evidenceImagesNotice(images), staged: rest.code !== 0 };
|
|
3536
3562
|
}
|
|
3537
|
-
/**
|
|
3538
|
-
* `claude --output-format json` returns an envelope: the result is the
|
|
3539
|
-
* summary, and the session id is what lets a malformed park be repaired by
|
|
3540
|
-
* resuming the conversation that produced it instead of paying for a new one.
|
|
3541
|
-
*/
|
|
3542
|
-
function envelope(stdout) {
|
|
3543
|
-
try {
|
|
3544
|
-
const parsed = JSON.parse(stdout);
|
|
3545
|
-
return {
|
|
3546
|
-
summary: typeof parsed.result === "string" && parsed.result.trim() !== ""
|
|
3547
|
-
? parsed.result.trim()
|
|
3548
|
-
: "unattended build",
|
|
3549
|
-
...(typeof parsed.session_id === "string" && parsed.session_id !== ""
|
|
3550
|
-
? { sessionId: parsed.session_id }
|
|
3551
|
-
: {}),
|
|
3552
|
-
};
|
|
3553
|
-
}
|
|
3554
|
-
catch {
|
|
3555
|
-
// An agent that printed something unparseable still did work — and a
|
|
3556
|
-
// session nobody can name simply cannot be resumed.
|
|
3557
|
-
return { summary: "unattended build" };
|
|
3558
|
-
}
|
|
3559
|
-
}
|
|
3560
3563
|
/**
|
|
3561
3564
|
* What a non-zero agent exit means, in words a person can act on: the
|
|
3562
3565
|
* harness's own result line names the ending (a turn ceiling, an error
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Deterministic JSON for digests and equality. These bytes are sealed into approvals and stored digests, so neither
|
|
3
|
+
* function may change its output for any input. The two differ on purpose and must stay distinct:
|
|
4
|
+
*
|
|
5
|
+
* - `canonicalJson` drops object keys whose value is undefined and returns JSON.stringify's own answer for a bare
|
|
6
|
+
* value (an undefined array element therefore renders as nothing: `[,1]`).
|
|
7
|
+
* - `canonicalJsonUndefinedAsNull` keeps every key and renders an undefined (or otherwise unserializable) value as
|
|
8
|
+
* `null` — the planner contract's bytes.
|
|
9
|
+
*
|
|
10
|
+
* Both sort object keys by UTF-16 code units and keep arrays in order.
|
|
11
|
+
*/
|
|
12
|
+
export declare function canonicalJson(value: unknown): string;
|
|
13
|
+
/** Stable JSON for hashing and equality only — no filling, no trimming; undefined reads as null. */
|
|
14
|
+
export declare function canonicalJsonUndefinedAsNull(value: unknown): string;
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Deterministic JSON for digests and equality. These bytes are sealed into approvals and stored digests, so neither
|
|
3
|
+
* function may change its output for any input. The two differ on purpose and must stay distinct:
|
|
4
|
+
*
|
|
5
|
+
* - `canonicalJson` drops object keys whose value is undefined and returns JSON.stringify's own answer for a bare
|
|
6
|
+
* value (an undefined array element therefore renders as nothing: `[,1]`).
|
|
7
|
+
* - `canonicalJsonUndefinedAsNull` keeps every key and renders an undefined (or otherwise unserializable) value as
|
|
8
|
+
* `null` — the planner contract's bytes.
|
|
9
|
+
*
|
|
10
|
+
* Both sort object keys by UTF-16 code units and keep arrays in order.
|
|
11
|
+
*/
|
|
12
|
+
export function canonicalJson(value) {
|
|
13
|
+
if (Array.isArray(value))
|
|
14
|
+
return `[${value.map(canonicalJson).join(",")}]`;
|
|
15
|
+
if (value !== null && typeof value === "object") {
|
|
16
|
+
const entries = Object.entries(value)
|
|
17
|
+
.filter(([, v]) => v !== undefined)
|
|
18
|
+
.sort(([a], [b]) => (a < b ? -1 : a > b ? 1 : 0))
|
|
19
|
+
.map(([k, v]) => `${JSON.stringify(k)}:${canonicalJson(v)}`);
|
|
20
|
+
return `{${entries.join(",")}}`;
|
|
21
|
+
}
|
|
22
|
+
return JSON.stringify(value);
|
|
23
|
+
}
|
|
24
|
+
/** Stable JSON for hashing and equality only — no filling, no trimming; undefined reads as null. */
|
|
25
|
+
export function canonicalJsonUndefinedAsNull(value) {
|
|
26
|
+
if (Array.isArray(value))
|
|
27
|
+
return `[${value.map(canonicalJsonUndefinedAsNull).join(",")}]`;
|
|
28
|
+
if (value !== null && typeof value === "object") {
|
|
29
|
+
const body = value;
|
|
30
|
+
return `{${Object.keys(body)
|
|
31
|
+
.sort()
|
|
32
|
+
.map(key => `${JSON.stringify(key)}:${canonicalJsonUndefinedAsNull(body[key])}`)
|
|
33
|
+
.join(",")}}`;
|
|
34
|
+
}
|
|
35
|
+
return JSON.stringify(value) ?? "null";
|
|
36
|
+
}
|
package/dist/chat-actions.js
CHANGED
|
@@ -16,7 +16,6 @@ import { changeKnowledge, knowledgeVersion, knowledgeView, } from "./project-kno
|
|
|
16
16
|
import { passwordlessSessionFor, approve, authenticateApprover, describeScope, scopeAuthorityOf, freshnessFor, spendingOnlyOnSuccess, ssoNowAvailable, withFreshness, } from "./scope.js";
|
|
17
17
|
import { readVerifiedArtifact, scanForSecrets } from "./evidence.js";
|
|
18
18
|
import { readAuthModeStrict } from "./keys.js";
|
|
19
|
-
import { parseProof } from "./proof.js";
|
|
20
19
|
import { acceptAndCompleteAsOperator, assignmentOf } from "./assignment.js";
|
|
21
20
|
import { getDecision, recordDecision, retireDecision } from "./project-memory.js";
|
|
22
21
|
import { resumeTaskStop, taskControlOf } from "./task-control.js";
|
|
@@ -26,7 +25,7 @@ import { saveScript, scriptDigest, validateScript } from "./flow-scripts.js";
|
|
|
26
25
|
import { addCardToFlow, advanceFlows, cancelFlowCard, crossProjectProblem, decideFlowCard, flowCardHref, flowCardText, flowDefinitionOf, moveCardInFlow } from "./flow-engine.js";
|
|
27
26
|
import { chooseFlowCard, flowPersonOf } from "./flow-send.js";
|
|
28
27
|
import { assignFlowCard, commentOnFlowCard, flowPeople, mentionsIn, watchFlowCard } from "./flow-people.js";
|
|
29
|
-
import { addFlowTriggerTo, describeTrigger, readLinearKey, removeFlowTrigger,
|
|
28
|
+
import { addFlowTriggerTo, describeTrigger, readLinearKey, removeFlowTrigger, takesDeliveries, triggerConfigOf, validateTriggerConfig } from "./flow-triggers.js";
|
|
30
29
|
import { describeSchedule, parseSchedule } from "./flow-schedule.js";
|
|
31
30
|
import { dirname } from "node:path";
|
|
32
31
|
import { handleOf, parseSoul, SOUL_CHARS, SUBAGENT_TEMPLATES, subagentLabel } from "./subagents.js";
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import type { ChatBinding, ChatState } from "./chat-delivery-state.js";
|
|
2
|
+
import type { ChatEventBody } from "./contracts/chat-content.js";
|
|
3
|
+
import type { ChatApp } from "./contracts/flow.js";
|
|
4
|
+
/**
|
|
5
|
+
* Whether a received message or tap, already proved to come from this app, may be queued — and under which binding.
|
|
6
|
+
* A room (any conversation that isn't a direct message) is taken only while it follows a conversation, for the words
|
|
7
|
+
* that make it follow one, or when it feeds a flow (v89: anyone's message then, as a card with no say over anything).
|
|
8
|
+
* A pairing message comes only from someone unpaired, in a direct message; everything else only from a live paired
|
|
9
|
+
* member in their own conversation or a followed room. Null when it is not taken.
|
|
10
|
+
*/
|
|
11
|
+
export declare function admitChatEvent(state: ChatState, app: ChatApp, installation: string, event: {
|
|
12
|
+
kind: "message" | "pair" | "action";
|
|
13
|
+
member: string;
|
|
14
|
+
channel: string;
|
|
15
|
+
isRoom: boolean;
|
|
16
|
+
payload: ChatEventBody;
|
|
17
|
+
}): {
|
|
18
|
+
binding: ChatBinding | null;
|
|
19
|
+
} | null;
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { channelInbox } from "./chat-inbox.js";
|
|
2
|
+
import { roomCommand } from "./chat-rooms.js";
|
|
3
|
+
/**
|
|
4
|
+
* Whether a received message or tap, already proved to come from this app, may be queued — and under which binding.
|
|
5
|
+
* A room (any conversation that isn't a direct message) is taken only while it follows a conversation, for the words
|
|
6
|
+
* that make it follow one, or when it feeds a flow (v89: anyone's message then, as a card with no say over anything).
|
|
7
|
+
* A pairing message comes only from someone unpaired, in a direct message; everything else only from a live paired
|
|
8
|
+
* member in their own conversation or a followed room. Null when it is not taken.
|
|
9
|
+
*/
|
|
10
|
+
export function admitChatEvent(state, app, installation, event) {
|
|
11
|
+
const { kind, member, channel, isRoom, payload } = event;
|
|
12
|
+
const words = "text" in payload ? payload.text ?? "" : "";
|
|
13
|
+
const inbox = isRoom && kind === "message" ? channelInbox(state.store, app, installation, channel, words) : { watched: false, command: false };
|
|
14
|
+
const roomish = isRoom && (state.room(installation, channel) !== null || (kind === "message" && roomCommand(words) !== null) || inbox.watched || inbox.command);
|
|
15
|
+
if (isRoom && !roomish)
|
|
16
|
+
return null;
|
|
17
|
+
const binding = state.bindingFor(installation, member);
|
|
18
|
+
const open = inbox.watched && !inbox.command;
|
|
19
|
+
if (kind === "pair" ? !!binding || isRoom : !open && (!binding || !state.live(binding) || (binding.channel !== channel && !roomish)))
|
|
20
|
+
return null;
|
|
21
|
+
return { binding };
|
|
22
|
+
}
|
package/dist/chat-channel.js
CHANGED
|
@@ -9,7 +9,7 @@ import { LEAD_TOOL_SCHEMAS } from "./lead-tools.js";
|
|
|
9
9
|
import { verifyApproverStanding } from "./principal.js";
|
|
10
10
|
import { canonicalProject } from "./project.js";
|
|
11
11
|
import { phoneText, projectLabel } from "./telegram-status.js";
|
|
12
|
-
import { CHAT_CONTROLS, chatControlHref,
|
|
12
|
+
import { CHAT_CONTROLS, chatControlHref, isChatControl, } from "./chat-controls.js";
|
|
13
13
|
import { CHAT_TASK_ACTIONS, isChatTaskAction } from "./chat-task-actions.js";
|
|
14
14
|
export function channelRepos(store, approver, registry) {
|
|
15
15
|
const seen = new Set();
|
package/dist/chat-delivery.js
CHANGED
|
@@ -23,7 +23,6 @@ import { phoneText, PHONE_HELP, phoneCommand, phoneStatus, phoneTaskView, phoneT
|
|
|
23
23
|
import { applyRoomInbound, conversationRow, roomCardApprover, roomCommand, roomGrantAllowed, roomMessagesAfter, roomMessageText, teamDomain } from "./chat-rooms.js";
|
|
24
24
|
import { isTelegramProgressNotification, proposalTaskOf } from "./store.js";
|
|
25
25
|
import { isShotsKind, resultShotsFor } from "./result-shots.js";
|
|
26
|
-
import { CHAT_APP_NAMES } from "./flow-triggers.js";
|
|
27
26
|
import { answersPrompt, applyDecideFeedback, applyDecideTap, decideFallbackLink, decideOffer, decideTargetOf, hasLiveDecideTokens, linksFor, mergedText, mintDecideButtons, offerFingerprint, openPromptFor, recordChatMerge } from "./chat-decide.js";
|
|
28
27
|
import { mergePullRequest } from "./pull-request-flow.js";
|
|
29
28
|
import { attentionSubjectOf, neverHeld, watchedPing } from "./agent-board.js";
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/** A pause that ends early, and quietly, when `signal` aborts. */
|
|
2
|
+
export declare const abortablePause: (signal: AbortSignal) => (ms: number) => Promise<void>;
|
|
3
|
+
/**
|
|
4
|
+
* Works the queue while this connection stays the current, leased one: each pass takes in saved events, plans room
|
|
5
|
+
* messages, delivers one part and plans notifications (when they are on), checking it is still current between steps.
|
|
6
|
+
* A pass waits while the connection isn't ready (offline, or the provider asked to retry later).
|
|
7
|
+
*/
|
|
8
|
+
export declare function workChatQueue(steps: {
|
|
9
|
+
same: () => boolean;
|
|
10
|
+
ready: () => boolean;
|
|
11
|
+
pause: (ms: number) => Promise<void>;
|
|
12
|
+
notifications: () => boolean;
|
|
13
|
+
process: () => Promise<unknown>;
|
|
14
|
+
rooms: () => Promise<unknown>;
|
|
15
|
+
deliver: () => Promise<unknown>;
|
|
16
|
+
notify: () => Promise<unknown>;
|
|
17
|
+
}): Promise<void>;
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/** The loop every hosted chat connection (Slack, Discord, Teams) runs over its saved queue. */
|
|
2
|
+
import { setTimeout as sleep } from "node:timers/promises";
|
|
3
|
+
/** A pause that ends early, and quietly, when `signal` aborts. */
|
|
4
|
+
export const abortablePause = (signal) => async (ms) => {
|
|
5
|
+
try {
|
|
6
|
+
await sleep(ms, undefined, { signal });
|
|
7
|
+
}
|
|
8
|
+
catch {
|
|
9
|
+
/* shutdown */
|
|
10
|
+
}
|
|
11
|
+
};
|
|
12
|
+
/**
|
|
13
|
+
* Works the queue while this connection stays the current, leased one: each pass takes in saved events, plans room
|
|
14
|
+
* messages, delivers one part and plans notifications (when they are on), checking it is still current between steps.
|
|
15
|
+
* A pass waits while the connection isn't ready (offline, or the provider asked to retry later).
|
|
16
|
+
*/
|
|
17
|
+
export async function workChatQueue(steps) {
|
|
18
|
+
while (steps.same()) {
|
|
19
|
+
if (!steps.ready()) {
|
|
20
|
+
await steps.pause(1000);
|
|
21
|
+
continue;
|
|
22
|
+
}
|
|
23
|
+
await steps.process();
|
|
24
|
+
if (steps.same())
|
|
25
|
+
await steps.rooms();
|
|
26
|
+
if (steps.same())
|
|
27
|
+
await steps.deliver();
|
|
28
|
+
if (steps.same() && steps.notifications())
|
|
29
|
+
await steps.notify();
|
|
30
|
+
await steps.pause(1000);
|
|
31
|
+
}
|
|
32
|
+
}
|
package/dist/chat-messages.js
CHANGED
|
@@ -8,7 +8,6 @@
|
|
|
8
8
|
*
|
|
9
9
|
* Imports the store's types only (no runtime cycle): the store builds its chats on it.
|
|
10
10
|
*/
|
|
11
|
-
import { join } from "node:path";
|
|
12
11
|
import { neverHeld, watchedPing } from "./agent-board.js";
|
|
13
12
|
import { ChatCore } from "./chat-core.js";
|
|
14
13
|
import { readChatMessageBody, readChatPart, savedChatEventBody, savedChatPart } from "./contracts/chat-content.js";
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import type { Store } from "./store.js";
|
|
2
|
+
import { type Html } from "./html.js";
|
|
3
|
+
import { type ChatIdentity } from "./chat-delivery-state.js";
|
|
4
|
+
export type ChatSettingsOptions = {
|
|
5
|
+
code?: string;
|
|
6
|
+
problem?: string;
|
|
7
|
+
now?: Date;
|
|
8
|
+
who?: string;
|
|
9
|
+
};
|
|
10
|
+
/** What differs between the Slack and Discord settings pages; everything else is one page. */
|
|
11
|
+
export type ChatSettingsPage = {
|
|
12
|
+
provider: "slack" | "discord";
|
|
13
|
+
label: string;
|
|
14
|
+
credentials: ChatIdentity | null;
|
|
15
|
+
/** How to create the app, shown before it is connected. */
|
|
16
|
+
setup: Html;
|
|
17
|
+
/** The connect form's fields around the password. */
|
|
18
|
+
connect: (password: Html) => Html;
|
|
19
|
+
/** The "Access and setup" paragraphs. */
|
|
20
|
+
access: Html;
|
|
21
|
+
};
|
|
22
|
+
/** Settings → Slack or Discord: connect the app once, then each teammate pairs their own account. */
|
|
23
|
+
export declare function chatSettingsHtml(store: Store, dir: string, options: ChatSettingsOptions, page: ChatSettingsPage): Html;
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { loadPrimary } from "./webhooks.js";
|
|
2
|
+
import { html, postForm } from "./html.js";
|
|
3
|
+
import { ChatState } from "./chat-delivery-state.js";
|
|
4
|
+
/** Settings → Slack or Discord: connect the app once, then each teammate pairs their own account. */
|
|
5
|
+
export function chatSettingsHtml(store, dir, options, page) {
|
|
6
|
+
const { provider, label, credentials } = page, state = new ChatState(store, provider), bindings = credentials ? state.bindings(credentials.installation).filter(one => state.live(one)) : [], binding = options.who === undefined ? null : bindings.find(one => one.approver === options.who) ?? null, others = bindings.length - (binding === null ? 0 : 1);
|
|
7
|
+
const runtime = credentials ? state.runtime(credentials.installation) : null;
|
|
8
|
+
const now = options.now ?? new Date(), live = runtime &&
|
|
9
|
+
runtime.leaseUntil !== null &&
|
|
10
|
+
runtime.leaseUntil > now.toISOString() &&
|
|
11
|
+
runtime.connected;
|
|
12
|
+
const password = html `<label>Your Toolroll password<input style="min-height:44px" type="password" name="password" autocomplete="current-password" required></label>`;
|
|
13
|
+
const post = (action, content) => postForm(`/settings/${provider}/${action}`, content, { attrs: { class: "card" } });
|
|
14
|
+
const parts = [];
|
|
15
|
+
if (!credentials) {
|
|
16
|
+
parts.push(html `<p>Manage projects and review results in a private ${label} conversation.</p>`, page.setup, post("connect", page.connect(password)));
|
|
17
|
+
}
|
|
18
|
+
else {
|
|
19
|
+
parts.push(html `<p><strong>${credentials.workspace}</strong> · ${live ? "Connected" : "Waiting for connection"}</p>`);
|
|
20
|
+
if (runtime?.problem)
|
|
21
|
+
parts.push(html `<p role="status">${String(runtime.problem)}</p>`);
|
|
22
|
+
if (!live && !runtime?.problem)
|
|
23
|
+
parts.push(html `<p>Keep the Toolroll worker running to receive ${label} messages.</p>`);
|
|
24
|
+
if (options.code) {
|
|
25
|
+
parts.push(html `<h2>Pair your account</h2><p>Send this in a direct message to your Toolroll ${label} app. It expires in 10 minutes.</p>`, html `<label>Pairing message<input type="text" readonly value="pair ${options.code}" autocomplete="off" style="width:100%;max-width:100%;font-size:14px;min-height:44px"></label>`, html `<a class="button-link" style="min-height:44px;white-space:nowrap" href="/settings/${provider}">Check connection</a>`);
|
|
26
|
+
}
|
|
27
|
+
else if (binding && state.live(binding)) {
|
|
28
|
+
const { pending, dropped: failed } = state.partCounts(binding.id);
|
|
29
|
+
parts.push(html `<p>Your ${label} account is paired.${others ? ` ${others} teammate${others === 1 ? " is" : "s are"} paired too.` : ""}${pending ? ` ${pending} replies waiting to send.` : ""}${failed ? ` ${failed} replies could not be delivered. Open the saved chat to recover them.` : ""}</p>`, html `<p>Try “What needs my attention?” or “Show the evidence for the latest result.”</p>`, html `<p><a href="/chat">Open saved chat</a></p>`, loadPrimary(process.env, dir) === provider
|
|
30
|
+
? html `<p>Task updates are sent here.</p>`
|
|
31
|
+
: post("alerts", html `<button type="submit">Send task updates here</button>`));
|
|
32
|
+
}
|
|
33
|
+
else {
|
|
34
|
+
parts.push(post("pair", html `<h2>Pair your account</h2><p>Your paired ${label} account can read your connected projects and confirm proposed changes. Password approvals still open in Toolroll.${others ? ` ${others} teammate${others === 1 ? " is" : "s are"} already paired.` : ""}</p>${password}<button type="submit">Create pairing code</button>`));
|
|
35
|
+
}
|
|
36
|
+
if (binding && state.live(binding) && !options.code)
|
|
37
|
+
parts.push(post("unpair", html `${password}<p class="meta">Unpairing ends every open button in your chat. Teammates are unaffected.</p><button type="submit">Unpair my account</button>`));
|
|
38
|
+
parts.push(html `<details><summary>Connection settings</summary>${post("disconnect", html `${password}<button type="submit">Disconnect ${label}</button>`)}</details>`);
|
|
39
|
+
}
|
|
40
|
+
return html `<section style="max-width:42rem;overflow-wrap:anywhere"><p><a href="/settings">Settings</a></p><h1>${label}</h1>${options.problem ? html `<p class="problem" role="alert">${options.problem}</p>` : ""}${parts}<details><summary>Access and setup</summary>${page.access}</details></section>`;
|
|
41
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Check access (v132, docs/plans/check-access.md): how far a project's setup and check commands reach beyond the
|
|
3
|
+
* plane's check fence. `sandboxed` (the default) is the fence as it is; `sign-ins` also reads the exact sign-in files
|
|
4
|
+
* of Claude, Codex and gh, under the project's network fence; `unsandboxed` runs them with no OS fence, trusted by the
|
|
5
|
+
* whole instance, for a project whose checks must create sandboxes themselves (Toolroll's own). Widening is an instance
|
|
6
|
+
* operator's act with a password typed now; any approver for the project may narrow it back.
|
|
7
|
+
*/
|
|
8
|
+
import { type NetworkProfile, type ProjectNetwork } from "./egress.js";
|
|
9
|
+
export declare const CHECK_ACCESS_LEVELS: readonly ["sandboxed", "sign-ins", "unsandboxed"];
|
|
10
|
+
export type CheckAccess = typeof CHECK_ACCESS_LEVELS[number];
|
|
11
|
+
export declare const CHECK_ACCESS_DEFAULT: CheckAccess;
|
|
12
|
+
export declare const isCheckAccess: (value: unknown) => value is CheckAccess;
|
|
13
|
+
/** A stored or sealed value as a level: anything missing or unreadable is the narrowest, never a wider one. */
|
|
14
|
+
export declare const checkAccessOf: (value: unknown) => CheckAccess;
|
|
15
|
+
export declare const widerThan: (a: CheckAccess, b: CheckAccess) => boolean;
|
|
16
|
+
/** The narrowest of several levels (none given: the default). */
|
|
17
|
+
export declare const narrowestAccess: (levels: readonly CheckAccess[]) => CheckAccess;
|
|
18
|
+
/** The level as a person reads it, in a sentence's middle ("Checks: …"). */
|
|
19
|
+
export declare function checkAccessWords(level: CheckAccess): string;
|
|
20
|
+
/** What each wider level means, said before anyone chooses it. */
|
|
21
|
+
export declare const CHECK_ACCESS_WARNINGS: Readonly<Record<Exclude<CheckAccess, "sandboxed">, string>>;
|
|
22
|
+
/** The services whose sign-ins a `sign-ins` check reads: Claude's and Codex's own hosts (egress.ts), and GitHub's. */
|
|
23
|
+
export declare const SIGN_IN_HOSTS: readonly string[];
|
|
24
|
+
/** Which of those a project's network doesn't allow (an open network allows all). The check's network is never widened
|
|
25
|
+
* for them: the operator adds them, as a network change of its own. */
|
|
26
|
+
export declare function signInHostsMissing(network: ProjectNetwork): string[];
|
|
27
|
+
/** The narrower of two networks: every host both allow (a wildcard allows what it covers), local services only if both
|
|
28
|
+
* do, open only if both are. A check never reaches what either its sealed network or the project's current one refuses. */
|
|
29
|
+
export declare function narrowestNetwork(a: NetworkProfile, b: NetworkProfile): NetworkProfile;
|
|
30
|
+
/** A build's checks' own network: what its approval sealed, less its agent's provider hosts (those are the agent's, never
|
|
31
|
+
* its checks'); narrower if a project host happens to be one of them. */
|
|
32
|
+
export declare function checkNetworkOf(sealed: NetworkProfile, provider: string, project: ProjectNetwork): NetworkProfile;
|
|
33
|
+
/** Off everywhere, for now (0.9.69): no `sign-ins`. Reading a sign-in file resolves its parent folders by path (Node
|
|
34
|
+
* has no openat), and each review round found another way a build or a check could swap a folder on that path between
|
|
35
|
+
* the checks and the read (on Linux a Claude build can write ~/.claude outright). Until that read is race-free, no
|
|
36
|
+
* check gets sign-ins: setting the level is refused, a level saved or sealed before (or copied in from another
|
|
37
|
+
* database) runs sandboxed, a scope seals sandboxed, and the sign-ins launch refuses first thing. The code behind it
|
|
38
|
+
* (signInGrants, readGrantedFile, the copies, the network fence) stays, guarded, for when such a reader exists; flip
|
|
39
|
+
* SIGN_INS_READY then (Linux stays off until its copies are race-free too). */
|
|
40
|
+
export declare const SIGN_INS_READY = false;
|
|
41
|
+
export declare const signInsAvailable: (platform?: NodeJS.Platform) => boolean;
|
|
42
|
+
/** Why sign-ins can't be chosen here, said plainly (null: they can). */
|
|
43
|
+
export declare const signInsUnavailableWords: (platform?: NodeJS.Platform) => string | null;
|
|
44
|
+
/** The level checks actually get on this platform: `sign-ins` where it isn't available is `sandboxed` (narrower, never
|
|
45
|
+
* wider). Everything that seals, launches or stamps a check's level goes through this. */
|
|
46
|
+
export declare const checkAccessHere: (level: CheckAccess, platform?: NodeJS.Platform) => CheckAccess;
|
|
47
|
+
/** How a project's checks actually run here, as the settings page and `project check-access` say it: with no OS fence
|
|
48
|
+
* on this machine and unconfined running allowed by an operator, without a sandbox whatever is saved (never said to be
|
|
49
|
+
* sandboxed); otherwise the saved level as this platform can give it. */
|
|
50
|
+
export declare function checkAccessRunsAs(level: CheckAccess, here: {
|
|
51
|
+
platform: NodeJS.Platform;
|
|
52
|
+
fenced: boolean;
|
|
53
|
+
unconfinedAllowed: boolean;
|
|
54
|
+
}): CheckAccess;
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Check access (v132, docs/plans/check-access.md): how far a project's setup and check commands reach beyond the
|
|
3
|
+
* plane's check fence. `sandboxed` (the default) is the fence as it is; `sign-ins` also reads the exact sign-in files
|
|
4
|
+
* of Claude, Codex and gh, under the project's network fence; `unsandboxed` runs them with no OS fence, trusted by the
|
|
5
|
+
* whole instance, for a project whose checks must create sandboxes themselves (Toolroll's own). Widening is an instance
|
|
6
|
+
* operator's act with a password typed now; any approver for the project may narrow it back.
|
|
7
|
+
*/
|
|
8
|
+
import { PROVIDER_HOSTS } from "./egress.js";
|
|
9
|
+
export const CHECK_ACCESS_LEVELS = ["sandboxed", "sign-ins", "unsandboxed"];
|
|
10
|
+
export const CHECK_ACCESS_DEFAULT = "sandboxed";
|
|
11
|
+
export const isCheckAccess = (value) => typeof value === "string" && CHECK_ACCESS_LEVELS.includes(value);
|
|
12
|
+
/** A stored or sealed value as a level: anything missing or unreadable is the narrowest, never a wider one. */
|
|
13
|
+
export const checkAccessOf = (value) => isCheckAccess(value) ? value : CHECK_ACCESS_DEFAULT;
|
|
14
|
+
const RANK = { sandboxed: 0, "sign-ins": 1, unsandboxed: 2 };
|
|
15
|
+
export const widerThan = (a, b) => RANK[a] > RANK[b];
|
|
16
|
+
/** The narrowest of several levels (none given: the default). */
|
|
17
|
+
export const narrowestAccess = (levels) => levels.reduce((low, one) => RANK[one] < RANK[low] ? one : low, levels.length === 0 ? CHECK_ACCESS_DEFAULT : "unsandboxed");
|
|
18
|
+
/** The level as a person reads it, in a sentence's middle ("Checks: …"). */
|
|
19
|
+
export function checkAccessWords(level) {
|
|
20
|
+
return level === "sandboxed" ? "sandboxed" : level === "sign-ins" ? "sandboxed, with model and GitHub sign-ins" : "not sandboxed";
|
|
21
|
+
}
|
|
22
|
+
/** What each wider level means, said before anyone chooses it. */
|
|
23
|
+
export const CHECK_ACCESS_WARNINGS = {
|
|
24
|
+
"sign-ins": "This project's checks can read your Claude, Codex and GitHub sign-ins. Its tests could use or send those credentials anywhere its network setting allows. Only choose this for projects you trust.",
|
|
25
|
+
unsandboxed: "This project's checks get your full access to this machine and to Toolroll itself, including its database and every sign-in. Only for a project whose checks must create sandboxes themselves, such as Toolroll's own.",
|
|
26
|
+
};
|
|
27
|
+
/** The services whose sign-ins a `sign-ins` check reads: Claude's and Codex's own hosts (egress.ts), and GitHub's. */
|
|
28
|
+
export const SIGN_IN_HOSTS = [...PROVIDER_HOSTS["claude"], ...PROVIDER_HOSTS["codex"], "github.com", "api.github.com"];
|
|
29
|
+
/** Which of those a project's network doesn't allow (an open network allows all). The check's network is never widened
|
|
30
|
+
* for them: the operator adds them, as a network change of its own. */
|
|
31
|
+
export function signInHostsMissing(network) {
|
|
32
|
+
if (network.mode === "open")
|
|
33
|
+
return [];
|
|
34
|
+
const covered = (host) => network.hosts.some(one => one === host || (one.startsWith("*.") && host.endsWith(one.slice(1))));
|
|
35
|
+
return SIGN_IN_HOSTS.filter(host => !covered(host));
|
|
36
|
+
}
|
|
37
|
+
/** The narrower of two networks: every host both allow (a wildcard allows what it covers), local services only if both
|
|
38
|
+
* do, open only if both are. A check never reaches what either its sealed network or the project's current one refuses. */
|
|
39
|
+
export function narrowestNetwork(a, b) {
|
|
40
|
+
const loopback = a.loopback === "any" && b.loopback === "any" ? "any" : "proxy-only";
|
|
41
|
+
if (a.mode === "open" && b.mode === "open")
|
|
42
|
+
return { version: 1, mode: "open", hosts: [], loopback };
|
|
43
|
+
if (a.mode === "open")
|
|
44
|
+
return { ...b, loopback };
|
|
45
|
+
if (b.mode === "open")
|
|
46
|
+
return { ...a, loopback };
|
|
47
|
+
const covers = (list, host) => list.some(one => one === host || (one.startsWith("*.") && (host.endsWith(one.slice(1)) || host === one)));
|
|
48
|
+
const hosts = [...new Set([...a.hosts.filter(host => covers(b.hosts, host)), ...b.hosts.filter(host => covers(a.hosts, host))])].sort();
|
|
49
|
+
return { version: 1, mode: "allowlist", hosts, loopback };
|
|
50
|
+
}
|
|
51
|
+
/** A build's checks' own network: what its approval sealed, less its agent's provider hosts (those are the agent's, never
|
|
52
|
+
* its checks'); narrower if a project host happens to be one of them. */
|
|
53
|
+
export function checkNetworkOf(sealed, provider, project) {
|
|
54
|
+
if (sealed.mode === "open")
|
|
55
|
+
return sealed;
|
|
56
|
+
// A provider host the project itself lists (an operator added it, for its sign-ins, say) is the checks' too.
|
|
57
|
+
const agent = new Set((PROVIDER_HOSTS[provider] ?? []).filter(host => !project.hosts.includes(host)));
|
|
58
|
+
return { ...sealed, hosts: sealed.hosts.filter(host => !agent.has(host)) };
|
|
59
|
+
}
|
|
60
|
+
/** Off everywhere, for now (0.9.69): no `sign-ins`. Reading a sign-in file resolves its parent folders by path (Node
|
|
61
|
+
* has no openat), and each review round found another way a build or a check could swap a folder on that path between
|
|
62
|
+
* the checks and the read (on Linux a Claude build can write ~/.claude outright). Until that read is race-free, no
|
|
63
|
+
* check gets sign-ins: setting the level is refused, a level saved or sealed before (or copied in from another
|
|
64
|
+
* database) runs sandboxed, a scope seals sandboxed, and the sign-ins launch refuses first thing. The code behind it
|
|
65
|
+
* (signInGrants, readGrantedFile, the copies, the network fence) stays, guarded, for when such a reader exists; flip
|
|
66
|
+
* SIGN_INS_READY then (Linux stays off until its copies are race-free too). */
|
|
67
|
+
export const SIGN_INS_READY = false;
|
|
68
|
+
export const signInsAvailable = (platform = process.platform) => SIGN_INS_READY && platform !== "linux";
|
|
69
|
+
/** Why sign-ins can't be chosen here, said plainly (null: they can). */
|
|
70
|
+
export const signInsUnavailableWords = (platform = process.platform) => signInsAvailable(platform) ? null
|
|
71
|
+
: "Not available yet: sign-ins can't be shared with checks safely. Choose Sandboxed or Not sandboxed.";
|
|
72
|
+
/** The level checks actually get on this platform: `sign-ins` where it isn't available is `sandboxed` (narrower, never
|
|
73
|
+
* wider). Everything that seals, launches or stamps a check's level goes through this. */
|
|
74
|
+
export const checkAccessHere = (level, platform = process.platform) => level === "sign-ins" && !signInsAvailable(platform) ? CHECK_ACCESS_DEFAULT : level;
|
|
75
|
+
/** How a project's checks actually run here, as the settings page and `project check-access` say it: with no OS fence
|
|
76
|
+
* on this machine and unconfined running allowed by an operator, without a sandbox whatever is saved (never said to be
|
|
77
|
+
* sandboxed); otherwise the saved level as this platform can give it. */
|
|
78
|
+
export function checkAccessRunsAs(level, here) {
|
|
79
|
+
return !here.fenced && here.unconfinedAllowed ? "unsandboxed" : checkAccessHere(level, here.platform);
|
|
80
|
+
}
|
package/dist/claim.js
CHANGED
|
@@ -749,7 +749,7 @@ export const MAX_STRIKES = 3;
|
|
|
749
749
|
function sealInterrupted(store, leaseId, args) {
|
|
750
750
|
const { runId, taskId, stopRun, now } = args;
|
|
751
751
|
const db = store.handle;
|
|
752
|
-
|
|
752
|
+
openAttempt(store, leaseId, runId, "an interruption");
|
|
753
753
|
// A lease the world moved past still ends its run as interrupted — that IS the honest ending — touching no task state.
|
|
754
754
|
const fenced = !releaseAs(store, leaseId, "interrupted", now);
|
|
755
755
|
if (args.message !== undefined && args.message.trim() !== "") {
|
|
@@ -797,7 +797,7 @@ function sealInterrupted(store, leaseId, args) {
|
|
|
797
797
|
function sealHandBack(store, leaseId, args) {
|
|
798
798
|
const { runId, taskId, now } = args;
|
|
799
799
|
const db = store.handle;
|
|
800
|
-
|
|
800
|
+
openAttempt(store, leaseId, runId, "a hand-back");
|
|
801
801
|
// A lease the world moved past still ends its run as interrupted — that IS the honest ending — touching no task state.
|
|
802
802
|
const fenced = !releaseAs(store, leaseId, "interrupted", now);
|
|
803
803
|
if (args.message.trim() !== "")
|
|
@@ -974,7 +974,6 @@ const PLAN_BACKOFF_MS = [60_000, 2 * 60_000, 4 * 60_000];
|
|
|
974
974
|
*/
|
|
975
975
|
function sealPlan(store, leaseId, args) {
|
|
976
976
|
const { runId, taskId, plan, now } = args;
|
|
977
|
-
const db = store.handle;
|
|
978
977
|
// The ingestion record (contract handoff, task 1) is composed here from
|
|
979
978
|
// the filed terms and the drafted plan; its file and row land inside the
|
|
980
979
|
// transaction below, only after the stale-source check admits the draft.
|
|
@@ -1199,7 +1198,7 @@ function describeSourceDrift(source, current) {
|
|
|
1199
1198
|
*/
|
|
1200
1199
|
function sealRevision(store, leaseId, args) {
|
|
1201
1200
|
const { runId, taskId, taskRef, revision, now } = args;
|
|
1202
|
-
|
|
1201
|
+
openAttempt(store, leaseId, runId, "a plan revision", "builder");
|
|
1203
1202
|
const stopped = interruptIfStopped(store, { leaseId, runId, taskId, now });
|
|
1204
1203
|
if (stopped !== null)
|
|
1205
1204
|
return { ok: false, reason: "stopped" };
|
|
@@ -1249,7 +1248,6 @@ function sealRevision(store, leaseId, args) {
|
|
|
1249
1248
|
*/
|
|
1250
1249
|
function sealPlanFailure(store, leaseId, args) {
|
|
1251
1250
|
const { runId, taskId, failure: kind, message, now } = args;
|
|
1252
|
-
const db = store.handle;
|
|
1253
1251
|
const run = openAttempt(store, leaseId, runId, "a failure", "planner");
|
|
1254
1252
|
const stopped = interruptIfStopped(store, { leaseId, runId, taskId, now });
|
|
1255
1253
|
if (stopped !== null)
|
|
@@ -1331,7 +1329,6 @@ function sealPlanFailure(store, leaseId, args) {
|
|
|
1331
1329
|
*/
|
|
1332
1330
|
function sealScout(store, leaseId, args) {
|
|
1333
1331
|
const { runId, taskId, report, now } = args;
|
|
1334
|
-
const db = store.handle;
|
|
1335
1332
|
const run = openAttempt(store, leaseId, runId, "a report", "scout");
|
|
1336
1333
|
const stopped = interruptIfStopped(store, { leaseId, runId, taskId, now });
|
|
1337
1334
|
if (stopped !== null)
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { ExecResult, run } from "./exec.js";
|
|
2
|
+
export type CommandRunner = (file: string, args: readonly string[], options: Parameters<typeof run>[2]) => Promise<ExecResult>;
|
|
3
|
+
/** No tools, no permission prompts, no MCP servers, nothing kept: a `claude -p` call that can only answer. */
|
|
4
|
+
export declare const CLAUDE_NO_TOOLS_ARGS: readonly string[];
|
|
5
|
+
/** `--model <model>`, or nothing for the CLI's own default. */
|
|
6
|
+
export declare const claudeModelArgs: (model: string) => string[];
|
|
7
|
+
/** What a failed call's output says when this computer's Claude isn't signed in. */
|
|
8
|
+
export declare const CLAUDE_SIGNED_OUT: RegExp;
|
|
9
|
+
/** `claude -p --output-format json` stdout as its one object, or null. */
|
|
10
|
+
export declare function claudeJsonBody(stdout: string, cap: number): Record<string, unknown> | null;
|
|
11
|
+
/** Runs `work` in a fresh temporary folder named `<prefix>…`, removed afterwards whatever happens. */
|
|
12
|
+
export declare function inScratchDir<T>(prefix: string, work: (dir: string) => Promise<T>): Promise<T>;
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The parts every bare `claude -p` call shares: a throwaway working folder, the flags that leave it with no tools,
|
|
3
|
+
* no MCP servers and no saved session, and reading its one JSON reply. Each caller still builds its own argv around
|
|
4
|
+
* these, in its own order.
|
|
5
|
+
*/
|
|
6
|
+
import { Buffer } from "node:buffer";
|
|
7
|
+
import { mkdtempSync, rmSync } from "node:fs";
|
|
8
|
+
import { tmpdir } from "node:os";
|
|
9
|
+
import { join } from "node:path";
|
|
10
|
+
import { strictJsonParse } from "./converse.js";
|
|
11
|
+
/** No tools, no permission prompts, no MCP servers, nothing kept: a `claude -p` call that can only answer. */
|
|
12
|
+
export const CLAUDE_NO_TOOLS_ARGS = [
|
|
13
|
+
"--safe-mode", "--no-session-persistence", "--tools", "", "--permission-mode", "dontAsk",
|
|
14
|
+
"--permission-prompts", "none", "--strict-mcp-config", "--mcp-config", '{"mcpServers":{}}',
|
|
15
|
+
];
|
|
16
|
+
/** `--model <model>`, or nothing for the CLI's own default. */
|
|
17
|
+
export const claudeModelArgs = (model) => (model === "default" ? [] : ["--model", model]);
|
|
18
|
+
/** What a failed call's output says when this computer's Claude isn't signed in. */
|
|
19
|
+
export const CLAUDE_SIGNED_OUT = /not logged in|login|authenticat/i;
|
|
20
|
+
/** `claude -p --output-format json` stdout as its one object, or null. */
|
|
21
|
+
export function claudeJsonBody(stdout, cap) {
|
|
22
|
+
const parsed = strictJsonParse(Buffer.from(stdout, "utf8"), cap, 12);
|
|
23
|
+
return parsed.ok && typeof parsed.value === "object" && parsed.value !== null && !Array.isArray(parsed.value) ? parsed.value : null;
|
|
24
|
+
}
|
|
25
|
+
/** Runs `work` in a fresh temporary folder named `<prefix>…`, removed afterwards whatever happens. */
|
|
26
|
+
export async function inScratchDir(prefix, work) {
|
|
27
|
+
const dir = mkdtempSync(join(tmpdir(), prefix));
|
|
28
|
+
try {
|
|
29
|
+
return await work(dir);
|
|
30
|
+
}
|
|
31
|
+
finally {
|
|
32
|
+
rmSync(dir, { recursive: true, force: true });
|
|
33
|
+
}
|
|
34
|
+
}
|