toolroll 0.9.63 → 0.9.65
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 +51 -0
- package/README.md +15 -5
- package/dist/acp-agents.d.ts +43 -0
- package/dist/acp-agents.js +62 -0
- package/dist/acp-attest.d.ts +44 -0
- package/dist/acp-attest.js +269 -0
- package/dist/acp-check.d.ts +28 -0
- package/dist/acp-check.js +410 -0
- package/dist/acp-client.d.ts +163 -0
- package/dist/acp-client.js +738 -0
- package/dist/acp-inspect.d.ts +23 -0
- package/dist/acp-inspect.js +109 -0
- package/dist/agent-board.d.ts +19 -6
- package/dist/agent-board.js +30 -9
- package/dist/agent-fence.d.ts +50 -2
- package/dist/agent-fence.js +157 -10
- package/dist/agent-pulse.js +9 -2
- package/dist/agent-state-words.d.ts +19 -0
- package/dist/agent-state-words.js +20 -0
- package/dist/agent-state.d.ts +65 -0
- package/dist/agent-state.js +229 -0
- package/dist/agentconfig.js +36 -13
- package/dist/agents-board.d.ts +34 -2
- package/dist/agents-board.js +110 -1
- package/dist/approval-rules-ui.js +2 -2
- package/dist/assignment-delivery.d.ts +14 -0
- package/dist/assignment-delivery.js +13 -0
- package/dist/assignment.js +13 -0
- package/dist/attest.js +7 -0
- package/dist/backup-ui.js +2 -2
- package/dist/browser/THIRD_PARTY_NOTICES.txt +48 -0
- package/dist/browser/terminal.css +37 -0
- package/dist/browser/terminal.js +117 -0
- package/dist/browser/workspace.css +2 -2
- package/dist/browser/workspace.js +42 -38
- package/dist/browser-shell.js +6 -3
- package/dist/browser-workspace.d.ts +235 -1
- package/dist/browser-workspace.js +60 -0
- package/dist/builder.d.ts +6 -1
- package/dist/builder.js +34 -13
- package/dist/chat-actions.d.ts +13 -1
- package/dist/chat-actions.js +197 -15
- package/dist/chat-ask.d.ts +4 -0
- package/dist/chat-ask.js +14 -0
- package/dist/chat-channel.js +20 -0
- package/dist/chat-continuity.js +5 -4
- package/dist/chat-decide.d.ts +0 -14
- package/dist/chat-decide.js +4 -1
- package/dist/chat-delivery.d.ts +9 -0
- package/dist/chat-delivery.js +45 -6
- package/dist/chat-messages.d.ts +8 -0
- package/dist/chat-messages.js +36 -4
- package/dist/check-levels-ui.js +2 -2
- package/dist/checkout-proof.d.ts +28 -0
- package/dist/checkout-proof.js +62 -0
- package/dist/claim.d.ts +12 -0
- package/dist/claim.js +19 -0
- package/dist/cli-help.d.ts +1 -1
- package/dist/cli-help.js +4 -1
- package/dist/cli.d.ts +3 -0
- package/dist/cli.js +21 -2
- package/dist/coding-ui.js +6 -4
- package/dist/console-instance.d.ts +124 -0
- package/dist/console-instance.js +337 -0
- package/dist/contracts/chat-actions.d.ts +16 -0
- package/dist/contracts/chat-actions.js +4 -0
- package/dist/contracts/chat-content.d.ts +19 -0
- package/dist/contracts/chat-content.js +6 -0
- package/dist/contracts/console-api.d.ts +12 -4
- package/dist/contracts/console-api.js +12 -4
- package/dist/contracts/flow-card.d.ts +4 -0
- package/dist/contracts/flow-card.js +3 -0
- package/dist/contracts/flow.d.ts +46 -0
- package/dist/contracts/flow.js +11 -2
- package/dist/contracts/lead-tools.d.ts +92 -0
- package/dist/contracts/lead-tools.js +13 -0
- package/dist/contracts/provider.d.ts +11 -1
- package/dist/contracts/provider.js +1 -1
- package/dist/contracts/route.d.ts +76 -1
- package/dist/contracts/route.js +1 -1
- package/dist/contracts/store-json.d.ts +2 -0
- package/dist/control-setup.d.ts +1 -1
- package/dist/control-ui.js +2 -2
- package/dist/converse.d.ts +16 -0
- package/dist/converse.js +32 -4
- package/dist/demo-chat.js +4 -4
- package/dist/demo.js +2 -2
- package/dist/desktop-update-gate.js +6 -0
- package/dist/desktop-update.d.ts +1 -1
- package/dist/discord-chat.js +4 -1
- package/dist/doctor.js +1 -1
- package/dist/egress-bridge.d.ts +17 -0
- package/dist/egress-bridge.js +111 -0
- package/dist/egress.d.ts +119 -0
- package/dist/egress.js +423 -0
- package/dist/exec.d.ts +78 -0
- package/dist/exec.js +367 -9
- package/dist/export-ui.js +2 -2
- package/dist/flow-engine.js +24 -5
- package/dist/flow-gallery-ui.js +2 -2
- package/dist/flow-gallery.js +27 -8
- package/dist/flow-share.js +16 -5
- package/dist/flow-triggers.d.ts +77 -8
- package/dist/flow-triggers.js +517 -70
- package/dist/flows-cli.js +8 -2
- package/dist/flows-ui.d.ts +0 -2
- package/dist/flows-ui.js +52 -4
- package/dist/flows.d.ts +4 -0
- package/dist/flows.js +9 -1
- package/dist/html.d.ts +9 -0
- package/dist/html.js +11 -0
- package/dist/invoke.d.ts +17 -0
- package/dist/invoke.js +217 -21
- package/dist/keys.d.ts +18 -14
- package/dist/keys.js +5 -0
- package/dist/lead-capabilities.js +3 -2
- package/dist/lead-contract.js +2 -0
- package/dist/lead-doors.d.ts +2 -0
- package/dist/lead-doors.js +7 -0
- package/dist/lead-follow.d.ts +2 -0
- package/dist/lead-follow.js +58 -2
- package/dist/lead-progress.js +1 -0
- package/dist/lead-queue-runtime.d.ts +46 -0
- package/dist/lead-queue-runtime.js +191 -0
- package/dist/lead-queue.d.ts +104 -0
- package/dist/lead-queue.js +187 -0
- package/dist/lead-sweep.d.ts +67 -0
- package/dist/lead-sweep.js +99 -0
- package/dist/lead-tools.d.ts +22 -0
- package/dist/lead-tools.js +152 -1
- package/dist/lead.d.ts +13 -5
- package/dist/lead.js +89 -29
- package/dist/limits-ui.d.ts +8 -1
- package/dist/limits-ui.js +26 -0
- package/dist/live-bus.d.ts +8 -0
- package/dist/live-bus.js +40 -4
- package/dist/live-detail.d.ts +27 -0
- package/dist/live-detail.js +57 -0
- package/dist/live-notes.d.ts +20 -0
- package/dist/live-notes.js +47 -0
- package/dist/live-records.d.ts +140 -0
- package/dist/live-records.js +935 -0
- package/dist/live-terminal.d.ts +258 -0
- package/dist/live-terminal.js +649 -0
- package/dist/live.d.ts +38 -27
- package/dist/live.js +142 -124
- package/dist/mcp-oauth.d.ts +6 -3
- package/dist/mcp-oauth.js +8 -4
- package/dist/memory-pass.js +2 -2
- package/dist/mobile-viewport.d.ts +5 -1
- package/dist/mobile-viewport.js +18 -2
- package/dist/model-catalog.js +4 -0
- package/dist/monitoring-ui.js +2 -2
- package/dist/needs-you.d.ts +1 -1
- package/dist/needs-you.js +11 -4
- package/dist/operate.d.ts +2 -0
- package/dist/operate.js +275 -17
- package/dist/passwordless.d.ts +23 -0
- package/dist/passwordless.js +26 -0
- package/dist/peek-cli.d.ts +12 -4
- package/dist/peek-cli.js +19 -11
- package/dist/phase-routing.js +10 -3
- package/dist/planner-source.d.ts +3 -0
- package/dist/planner-source.js +14 -0
- package/dist/planner.d.ts +2 -1
- package/dist/planner.js +7 -1
- package/dist/policy-ui.js +2 -2
- package/dist/policy.d.ts +1 -20
- package/dist/policy.js +29 -2
- package/dist/project-delete-ui.d.ts +12 -0
- package/dist/project-delete-ui.js +41 -9
- package/dist/project-delete.js +2 -2
- package/dist/project-memory.d.ts +3 -1
- package/dist/project-memory.js +57 -25
- package/dist/project-tools.d.ts +5 -3
- package/dist/project-tools.js +12 -9
- package/dist/provider-auth.d.ts +4 -3
- package/dist/provider-auth.js +5 -4
- package/dist/provider-connection.d.ts +2 -1
- package/dist/provider-connection.js +5 -0
- package/dist/provider.d.ts +25 -2
- package/dist/provider.js +63 -9
- package/dist/pull-request-ui.js +2 -2
- package/dist/push.js +1 -1
- package/dist/quick-start.d.ts +24 -0
- package/dist/quick-start.js +127 -0
- package/dist/release.d.ts +3 -0
- package/dist/release.js +14 -8
- package/dist/request-budget-ui.js +2 -2
- package/dist/result-review.d.ts +1 -1
- package/dist/result-review.js +9 -3
- package/dist/retention-ui.js +2 -2
- package/dist/retention.js +10 -1
- package/dist/run-efficiency.d.ts +73 -0
- package/dist/run-efficiency.js +228 -0
- package/dist/runner.d.ts +5 -1
- package/dist/runner.js +22 -1
- package/dist/scope.d.ts +170 -5
- package/dist/scope.js +251 -31
- package/dist/scout-net.d.ts +4 -1
- package/dist/scout-net.js +37 -12
- package/dist/scout.d.ts +20 -6
- package/dist/scout.js +27 -3
- package/dist/serve.js +247 -40
- package/dist/server/chat.d.ts +1 -1
- package/dist/server/chat.js +122 -31
- package/dist/server/chrome.d.ts +6 -1
- package/dist/server/chrome.js +18 -7
- package/dist/server/console.css +52 -38
- package/dist/server/flows.js +38 -8
- package/dist/server/guards.d.ts +1 -0
- package/dist/server/guards.js +23 -9
- package/dist/server/http.d.ts +7 -0
- package/dist/server/http.js +3 -2
- package/dist/server/live.d.ts +34 -1
- package/dist/server/live.js +39 -17
- package/dist/server/pages-assets.js +5 -0
- package/dist/server/pages.js +90 -13
- package/dist/server/people-tokens.d.ts +3 -0
- package/dist/server/people-tokens.js +55 -17
- package/dist/server/remote-hooks.js +12 -6
- package/dist/server/remote.d.ts +2 -1
- package/dist/server/remote.js +14 -5
- package/dist/server/render-chat.d.ts +12 -3
- package/dist/server/render-chat.js +47 -16
- package/dist/server/render-pages.d.ts +4 -1
- package/dist/server/render-pages.js +5 -4
- package/dist/server/render-people.d.ts +3 -2
- package/dist/server/render-people.js +3 -2
- package/dist/server/render-results.d.ts +18 -2
- package/dist/server/render-results.js +37 -28
- package/dist/server/render-settings.js +6 -5
- package/dist/server/render-tasks.d.ts +35 -10
- package/dist/server/render-tasks.js +110 -27
- package/dist/server/request-context.d.ts +2 -0
- package/dist/server/request-context.js +2 -1
- package/dist/server/route-table.d.ts +3 -0
- package/dist/server/route-table.js +46 -5
- package/dist/server/runtime.d.ts +33 -2
- package/dist/server/session.d.ts +25 -1
- package/dist/server/session.js +18 -4
- package/dist/server/settings.js +121 -10
- package/dist/server/sweeps.d.ts +4 -0
- package/dist/server/sweeps.js +58 -0
- package/dist/server/task-views.d.ts +2 -1
- package/dist/server/task-views.js +96 -6
- package/dist/server/tasks.js +320 -74
- package/dist/server/terminals.d.ts +31 -0
- package/dist/server/terminals.js +365 -0
- package/dist/setup-guide.js +3 -2
- package/dist/slack-chat.js +4 -1
- package/dist/spend-ui.js +3 -3
- package/dist/spend.js +2 -1
- package/dist/steer-capabilities.d.ts +32 -0
- package/dist/steer-capabilities.js +60 -0
- package/dist/steer-controls.d.ts +14 -0
- package/dist/steer-controls.js +36 -0
- package/dist/steer-status.js +2 -2
- package/dist/storage-ui.js +4 -4
- package/dist/store-baseline.d.ts +1 -1
- package/dist/store-baseline.js +1 -1
- package/dist/store.d.ts +430 -15
- package/dist/store.js +1041 -52
- package/dist/subagent-desk.js +1 -1
- package/dist/summary.js +3 -0
- package/dist/task-source.d.ts +58 -0
- package/dist/task-source.js +93 -0
- package/dist/team-leads.js +1 -1
- package/dist/teams-chat.js +4 -1
- package/dist/telegram-lead.d.ts +3 -0
- package/dist/telegram-lead.js +44 -7
- package/dist/telegram.d.ts +11 -2
- package/dist/telegram.js +51 -17
- package/dist/terminal-catalog.d.ts +11 -0
- package/dist/terminal-catalog.js +129 -0
- package/dist/text-limits.d.ts +2 -0
- package/dist/text-limits.js +2 -0
- package/dist/tool-broker.d.ts +2 -0
- package/dist/tool-broker.js +1 -0
- package/dist/toolroll-update-ui.js +2 -2
- package/dist/toolroll-update.js +13 -3
- package/dist/tools-ui.js +2 -2
- package/dist/webhook-limits.d.ts +23 -0
- package/dist/webhook-limits.js +30 -0
- package/dist/webhook-readers.d.ts +56 -0
- package/dist/webhook-readers.js +138 -0
- package/dist/webhook-signing.d.ts +27 -0
- package/dist/webhook-signing.js +119 -0
- package/dist/work-index.d.ts +29 -0
- package/dist/work-index.js +16 -2
- package/dist/workspace-revision.js +3 -1
- package/package.json +11 -2
package/dist/scope.d.ts
CHANGED
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
export { ACCEPTANCE_LIMITS, EVIDENCE_KINDS, type EvidenceKind } from "./contracts/acceptance-terms.js";
|
|
2
2
|
import { AsyncLocalStorage } from "node:async_hooks";
|
|
3
|
+
import { isAcpProfile, type AcpProviderId } from "./acp-agents.js";
|
|
4
|
+
export { isAcpProfile };
|
|
5
|
+
import { type NetworkProfile } from "./egress.js";
|
|
3
6
|
import { type EvidenceKind } from "./contracts/acceptance-terms.js";
|
|
4
7
|
import { type AcceptanceCriterion, type AcceptanceProblem, type ScopeTerms } from "./contracts/scope.js";
|
|
5
8
|
import type { Store, Mutation } from "./store.js";
|
|
@@ -42,6 +45,13 @@ export declare const GEMINI_LIMITS: {
|
|
|
42
45
|
readonly timeoutSeconds: 1200;
|
|
43
46
|
readonly repairTimeoutSeconds: 300;
|
|
44
47
|
};
|
|
48
|
+
/** An ACP agent runs under two clocks at once: the whole attempt, and silence (no line from the agent). */
|
|
49
|
+
export declare const ACP_LIMITS: {
|
|
50
|
+
readonly timeoutSeconds: 3600;
|
|
51
|
+
readonly idleTimeoutSeconds: 900;
|
|
52
|
+
readonly repairTimeoutSeconds: 900;
|
|
53
|
+
readonly repairIdleTimeoutSeconds: 300;
|
|
54
|
+
};
|
|
45
55
|
export type ClaudeProfile = {
|
|
46
56
|
provider: "claude";
|
|
47
57
|
/** The exact model string the argv carries. Never empty, never "default". */
|
|
@@ -61,6 +71,9 @@ export type ClaudeProfile = {
|
|
|
61
71
|
/** Exact model for repairs, or the stable literal "inherit" (= the
|
|
62
72
|
* build model, which is itself exact). */
|
|
63
73
|
repairModel: string;
|
|
74
|
+
/** What the build may reach (egress.ts), resolved when the scope was written; absent on approvals sealed before
|
|
75
|
+
* network policies, which run narrowed (egress.ts legacyNetwork). */
|
|
76
|
+
network?: NetworkProfile;
|
|
64
77
|
};
|
|
65
78
|
export type CodexShapedProfile = {
|
|
66
79
|
provider: "codex" | "openrouter";
|
|
@@ -77,6 +90,7 @@ export type CodexShapedProfile = {
|
|
|
77
90
|
timeoutKind?: "idle";
|
|
78
91
|
repairTimeoutSeconds: number;
|
|
79
92
|
repairModel: string;
|
|
93
|
+
network?: NetworkProfile;
|
|
80
94
|
};
|
|
81
95
|
export type GeminiProfile = {
|
|
82
96
|
provider: "gemini";
|
|
@@ -95,8 +109,40 @@ export type GeminiProfile = {
|
|
|
95
109
|
timeoutKind?: "idle";
|
|
96
110
|
repairTimeoutSeconds: number;
|
|
97
111
|
repairModel: string;
|
|
112
|
+
network?: NetworkProfile;
|
|
113
|
+
};
|
|
114
|
+
/**
|
|
115
|
+
* An agent over the Agent Client Protocol (acp-agents.ts, docs/plans/acp-adapter.md).
|
|
116
|
+
* Full access inside the OS fence is the only posture: nothing else could be
|
|
117
|
+
* made true (an agent may run commands without asking). There is no turn
|
|
118
|
+
* limit; both clocks bound every attempt, and a repair has its own pair.
|
|
119
|
+
*/
|
|
120
|
+
export type AcpProfile = {
|
|
121
|
+
provider: AcpProviderId;
|
|
122
|
+
/** Exact, and one the agent offered when it was checked. */
|
|
123
|
+
model: string;
|
|
124
|
+
permission: "full";
|
|
125
|
+
maxTurns: "unsupported";
|
|
126
|
+
repairMaxTurns: "unsupported";
|
|
127
|
+
timeoutSeconds: number;
|
|
128
|
+
idleTimeoutSeconds: number;
|
|
129
|
+
repairTimeoutSeconds: number;
|
|
130
|
+
repairIdleTimeoutSeconds: number;
|
|
131
|
+
repairModel: string;
|
|
132
|
+
network?: NetworkProfile;
|
|
133
|
+
};
|
|
134
|
+
export type ExecutionProfile = ClaudeProfile | CodexShapedProfile | GeminiProfile | AcpProfile;
|
|
135
|
+
/** Whether a profile's clock is a no-progress window (an ACP profile has both clocks; its overall one is a deadline). */
|
|
136
|
+
export declare function idleClock(profile: ExecutionProfile): boolean;
|
|
137
|
+
/**
|
|
138
|
+
* The watchdogs an attempt runs under, from its sealed profile and the overall bound the caller resolved: an ACP
|
|
139
|
+
* profile gets both clocks at once (the deadline and its silence window — a repair its own pair); the others one,
|
|
140
|
+
* a no-progress window when the profile says idle, else a deadline.
|
|
141
|
+
*/
|
|
142
|
+
export declare function watchdogsOf(profile: ExecutionProfile, timeoutMs: number, repair?: boolean): {
|
|
143
|
+
timeoutMs?: number;
|
|
144
|
+
idleTimeoutMs?: number;
|
|
98
145
|
};
|
|
99
|
-
export type ExecutionProfile = ClaudeProfile | CodexShapedProfile | GeminiProfile;
|
|
100
146
|
/** The durable unattended permission choices exposed by the console. The
|
|
101
147
|
* provider-specific argv is still what gets sealed into an approval; this
|
|
102
148
|
* small cross-provider type is only the operator-facing policy. */
|
|
@@ -461,6 +507,11 @@ export type ApproveResult = {
|
|
|
461
507
|
ok: false;
|
|
462
508
|
reason: "policy";
|
|
463
509
|
message: string;
|
|
510
|
+
}
|
|
511
|
+
/** Cockpit B2: a remembered password can't approve these terms (protected, escalated, or a quarantined filing). */
|
|
512
|
+
| {
|
|
513
|
+
ok: false;
|
|
514
|
+
reason: "password-now";
|
|
464
515
|
};
|
|
465
516
|
/**
|
|
466
517
|
* Whether this name-and-token pair is a person the store knows. Shared by
|
|
@@ -494,12 +545,114 @@ export declare function authenticateAccount(store: Store, by: string, secret: st
|
|
|
494
545
|
* approval, chat-action review, the console's own ceremonies). A typed
|
|
495
546
|
* password is always checked, and nothing else stands in.
|
|
496
547
|
*/
|
|
497
|
-
|
|
498
|
-
|
|
548
|
+
/**
|
|
549
|
+
* PASSWORD FRESHNESS (docs/plans/agents-cockpit.md B2), and the one place an empty password field is ever honoured.
|
|
550
|
+
*
|
|
551
|
+
* - An identity-provider re-check made **on request** (a step-up's re-authentication, never an ordinary sign-in) stands
|
|
552
|
+
* in for one password typed now, for ONE step-up of any kind, within five minutes: it is consumed by the act it
|
|
553
|
+
* proves (`sso-now`). Never reused across acts.
|
|
554
|
+
* - A password verified on this session, or that explicit re-check, stands in for fifteen minutes — but only inside an
|
|
555
|
+
* allowlisted act (`withFreshness`), only for this person, only on a direct local request (`fresh-password`,
|
|
556
|
+
* `fresh-sso`). Never extended by use.
|
|
557
|
+
* Every fact is read at the moment of the act (`FreshFacts` are live readers, not a snapshot), so a sign-out, a session
|
|
558
|
+
* revoked or rotated, a credential change or Forget made while a request was in flight is seen by it.
|
|
559
|
+
*/
|
|
560
|
+
export type FreshOp = "approve-standard" | "chat-send" | "reply-question";
|
|
561
|
+
/** "session": Passwordless is on and this is the person's own live browser session (docs/plans/passwordless.md). */
|
|
562
|
+
/** "remote-token" / "lead-token": a machine credential stood in for the person (a remote run's API token, a lead's
|
|
563
|
+
* token) — fine for ordinary acts it is allowed, never for a credential act (it would mint a way in with no password). */
|
|
564
|
+
export type AuthMethod = "password-now" | "sso-now" | "fresh-password" | "fresh-sso" | "session" | "cli-login" | "remote-token" | "lead-token";
|
|
565
|
+
/**
|
|
566
|
+
* The local CLI's remembered login, marked for the one command that read it (operate.ts credentialsFrom): with
|
|
567
|
+
* Passwordless on, a proof made with it records "cli-login" — never a typed password — and no credential act accepts it.
|
|
568
|
+
* Scoped to that command's async chain; a browser request in the console's own process never carries it.
|
|
569
|
+
*/
|
|
570
|
+
export declare const rememberedLogin: AsyncLocalStorage<{
|
|
571
|
+
login: {
|
|
572
|
+
name: string;
|
|
573
|
+
token: string;
|
|
574
|
+
} | null;
|
|
499
575
|
}>;
|
|
500
|
-
export declare const
|
|
501
|
-
|
|
576
|
+
export declare const FRESH_WINDOW_MS: number;
|
|
577
|
+
/** How long an explicit provider re-check stands in for one password typed now. */
|
|
578
|
+
export declare const SSO_NOW_MS: number;
|
|
579
|
+
export type FreshFacts = {
|
|
580
|
+
actor: string;
|
|
581
|
+
/** Straight from this computer: a loopback peer naming a loopback host, with no forwarding headers of any kind. */
|
|
582
|
+
local: boolean;
|
|
583
|
+
/** The request's route is on the freshness allowlist (route-table.ts `freshness: "allowlist"`): the table decides. */
|
|
584
|
+
allowlisted: boolean;
|
|
585
|
+
/** The session is still signed in, under the account's current credential generation, not revoked. */
|
|
586
|
+
live: () => boolean;
|
|
587
|
+
/** When a password was last verified on this session; when the provider last re-checked on request (window start);
|
|
588
|
+
* and an unconsumed re-check for one step-up. */
|
|
589
|
+
passwordAt: () => number | null;
|
|
590
|
+
ssoReauthAt: () => number | null;
|
|
591
|
+
ssoNow: () => number | null;
|
|
592
|
+
/** Spend the re-check; the answer puts it back (a refund) when the act it proved doesn't happen. */
|
|
593
|
+
consumeSsoNow: () => () => void;
|
|
594
|
+
/** Nothing before this counts (Forget). */
|
|
595
|
+
floor: () => number;
|
|
596
|
+
/** Record a verified password entry on this session (the console decides whether this request may start one). */
|
|
597
|
+
stamp: (at: number) => void;
|
|
598
|
+
};
|
|
599
|
+
export declare const freshFacts: AsyncLocalStorage<FreshFacts | null>;
|
|
600
|
+
/**
|
|
601
|
+
* Run an act a re-check may prove, spending the re-check only if the act happens: when it throws (a rolled-back
|
|
602
|
+
* transaction) or `failed` says it was refused, every re-check it spent is put back. ONLY for acts whose every effect
|
|
603
|
+
* is in one database transaction (an approval, a chat approval proposal, a chat turn opening): there, "refused" and
|
|
604
|
+
* "thrown" prove nothing happened. Every other step-up spends its re-check whatever comes of it (fail-closed: at worst
|
|
605
|
+
* the person confirms with their provider again). Synchronous acts only.
|
|
606
|
+
*/
|
|
607
|
+
export declare function spendingOnlyOnSuccess<T>(act: () => T, failed: (result: T) => boolean): T;
|
|
608
|
+
/** Run an allowlisted act: within it, a fresh password or re-check may stand in for one typed now. */
|
|
609
|
+
export declare function withFreshness<T>(op: FreshOp, act: () => T): T;
|
|
610
|
+
/** The freshness this act may stand on now, or null: only inside an allowlisted act on an allowlisted route, for this
|
|
611
|
+
* person, on a direct local request, on a live session, within the window, after any Forget. `preview` asks it from a
|
|
612
|
+
* page (a GET) about the allowlisted act its form posts to — for words only, never to authorise. */
|
|
613
|
+
export declare function freshnessFor(by: string, now?: number, preview?: boolean): {
|
|
614
|
+
method: "fresh-password" | "fresh-sso";
|
|
615
|
+
at: number;
|
|
616
|
+
} | null;
|
|
617
|
+
/** Whether an explicit provider re-check is waiting to stand in for one step-up (the page says "Confirmed with …"). */
|
|
618
|
+
export declare function ssoNowAvailable(by: string, now?: number): boolean;
|
|
619
|
+
/**
|
|
620
|
+
* Proof that this person is here now, for any account step-up (an approver's or not): a password typed now, or an
|
|
621
|
+
* explicit provider re-check (consumed). The core of every console step-up; `authenticateApprover` adds standing.
|
|
622
|
+
*/
|
|
623
|
+
export declare function proveAccountNow(store: Store, by: string, token: string, options?: {
|
|
624
|
+
credential?: boolean;
|
|
625
|
+
}): {
|
|
626
|
+
ok: true;
|
|
627
|
+
method: AuthMethod;
|
|
628
|
+
at: number;
|
|
629
|
+
} | {
|
|
630
|
+
ok: false;
|
|
631
|
+
reason: "no-approvers" | "not-an-approver";
|
|
632
|
+
};
|
|
633
|
+
/**
|
|
634
|
+
* A credential act (docs/plans/passwordless.md, the closed list: one that creates or widens a way in): only a password
|
|
635
|
+
* typed now, or a one-use identity-provider re-check, proves it — never a session, never a remembered password.
|
|
636
|
+
*/
|
|
637
|
+
/** Whether an empty password would pass for `by` right now because Passwordless is on and this is their live browser
|
|
638
|
+
* session. Read only — the act itself proves it again inside `proveAccountNow`. */
|
|
639
|
+
export declare function passwordlessSessionFor(store: Store, by: string): boolean;
|
|
640
|
+
/** Whether a proof is one a credential act accepts: a password typed now, or a one-use identity-provider re-check. */
|
|
641
|
+
export declare const credentialProof: (method: AuthMethod) => boolean;
|
|
642
|
+
export declare function proveCredential(store: Store, by: string, token: string, repo?: string | null): {
|
|
502
643
|
ok: true;
|
|
644
|
+
method: AuthMethod;
|
|
645
|
+
at: number;
|
|
646
|
+
} | {
|
|
647
|
+
ok: false;
|
|
648
|
+
reason: "no-approvers" | "not-an-approver";
|
|
649
|
+
};
|
|
650
|
+
export declare function authenticateApprover(store: Store, by: string, token: string, repo?: string | null, options?: {
|
|
651
|
+
credential?: boolean;
|
|
652
|
+
}): {
|
|
653
|
+
ok: true;
|
|
654
|
+
method: AuthMethod;
|
|
655
|
+
at: number;
|
|
503
656
|
} | {
|
|
504
657
|
ok: false;
|
|
505
658
|
reason: "no-approvers" | "not-an-approver";
|
|
@@ -548,6 +701,18 @@ export declare function fileAndSealUnderMode(store: Store, input: ScopeInput & {
|
|
|
548
701
|
detail?: string;
|
|
549
702
|
};
|
|
550
703
|
export declare function approve(store: Store, taskId: string, by: string, now: Date, sawDigest: string, token: string, mutation?: Mutation): ApproveResult;
|
|
704
|
+
/** The ledger act that records how an approval's person was proved (password-now, sso-now, fresh-password, fresh-sso). */
|
|
705
|
+
export declare const APPROVAL_PROOF = "approval proved";
|
|
706
|
+
/** The proof row's detail: when the person was proved, the generation, and the exact digest they said yes to. */
|
|
707
|
+
export declare const approvalProofDetail: (at: number, generation: number, digest: string) => string;
|
|
708
|
+
/** The proof of THIS approval: the approver's own row for these exact bytes (never another approval's words). */
|
|
709
|
+
export declare function approvalProofOf(store: Store, taskId: string, by: string, digest: string): {
|
|
710
|
+
method: string;
|
|
711
|
+
at: string;
|
|
712
|
+
} | null;
|
|
713
|
+
/** Whether a remembered password may approve these terms: one approver (no protected work), standard permissions
|
|
714
|
+
* (never escalated), and not filed by a coordinator, a lead or a scout (those keep their own password ceremony). */
|
|
715
|
+
export declare function freshlyApprovable(store: Store, taskId: string, scope: Scope, singleApprover: boolean): boolean;
|
|
551
716
|
/**
|
|
552
717
|
* Whether this task may be built.
|
|
553
718
|
*
|
package/dist/scope.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
// The rubric's vocabulary loads before anything that reaches the plan contract (see contracts/acceptance-terms.ts).
|
|
2
2
|
export { ACCEPTANCE_LIMITS, EVIDENCE_KINDS } from "./contracts/acceptance-terms.js";
|
|
3
|
+
import { passwordlessOn } from "./passwordless.js";
|
|
3
4
|
import { currentPasswordSource, passwordGuardOf, provenPasswordAccount } from "./sign-in-guard.js";
|
|
4
5
|
import { validateScopeText } from "./task-text.js";
|
|
5
6
|
import { contractChangesOf } from "./contract-changes.js";
|
|
@@ -8,6 +9,9 @@ import { projectAuthority } from "./project-access.js";
|
|
|
8
9
|
import { activeRemote } from "./remote-run.js";
|
|
9
10
|
import { claimActor, currentActor } from "./actor.js";
|
|
10
11
|
import { parseLeadToken } from "./api-tokens.js";
|
|
12
|
+
import { isAcpProfile, isAcpProvider } from "./acp-agents.js";
|
|
13
|
+
import { levelOfProfile } from "./policy.js";
|
|
14
|
+
export { isAcpProfile };
|
|
11
15
|
/**
|
|
12
16
|
* What a task is allowed to become, agreed before anything builds it.
|
|
13
17
|
*
|
|
@@ -34,6 +38,7 @@ import { parseLeadToken } from "./api-tokens.js";
|
|
|
34
38
|
* glance is the whole safety property.
|
|
35
39
|
*/
|
|
36
40
|
import { createHash, randomBytes, scryptSync, timingSafeEqual } from "node:crypto";
|
|
41
|
+
import { networkFromValue, networkWords } from "./egress.js";
|
|
37
42
|
import { z } from "zod";
|
|
38
43
|
import {} from "./contracts/acceptance-terms.js";
|
|
39
44
|
import { parseContract } from "./contracts/contract.js";
|
|
@@ -105,6 +110,27 @@ export const GEMINI_LIMITS = {
|
|
|
105
110
|
timeoutSeconds: 1200,
|
|
106
111
|
repairTimeoutSeconds: 300,
|
|
107
112
|
};
|
|
113
|
+
/** An ACP agent runs under two clocks at once: the whole attempt, and silence (no line from the agent). */
|
|
114
|
+
export const ACP_LIMITS = {
|
|
115
|
+
timeoutSeconds: 3600,
|
|
116
|
+
idleTimeoutSeconds: 900,
|
|
117
|
+
repairTimeoutSeconds: 900,
|
|
118
|
+
repairIdleTimeoutSeconds: 300,
|
|
119
|
+
};
|
|
120
|
+
/** Whether a profile's clock is a no-progress window (an ACP profile has both clocks; its overall one is a deadline). */
|
|
121
|
+
export function idleClock(profile) {
|
|
122
|
+
return !isAcpProfile(profile) && profile.timeoutKind === "idle";
|
|
123
|
+
}
|
|
124
|
+
/**
|
|
125
|
+
* The watchdogs an attempt runs under, from its sealed profile and the overall bound the caller resolved: an ACP
|
|
126
|
+
* profile gets both clocks at once (the deadline and its silence window — a repair its own pair); the others one,
|
|
127
|
+
* a no-progress window when the profile says idle, else a deadline.
|
|
128
|
+
*/
|
|
129
|
+
export function watchdogsOf(profile, timeoutMs, repair = false) {
|
|
130
|
+
if (isAcpProfile(profile))
|
|
131
|
+
return { timeoutMs, idleTimeoutMs: (repair ? profile.repairIdleTimeoutSeconds : profile.idleTimeoutSeconds) * 1000 };
|
|
132
|
+
return profile.timeoutKind === "idle" ? { idleTimeoutMs: timeoutMs } : { timeoutMs };
|
|
133
|
+
}
|
|
108
134
|
export const PROFILE_DIGEST_VERSION = 2;
|
|
109
135
|
/** Deterministic JSON: object keys sorted recursively, arrays in order. */
|
|
110
136
|
function canonicalJson(value) {
|
|
@@ -332,8 +358,13 @@ export function profileFromJson(json) {
|
|
|
332
358
|
const num = (v) => safePositiveInteger(v);
|
|
333
359
|
const clock = (v) => timerSafeSeconds(v);
|
|
334
360
|
const PROVIDER_KEYS = ["provider", "model", "maxTurns", "repairMaxTurns", "timeoutSeconds", "repairTimeoutSeconds", "repairModel"];
|
|
361
|
+
// The network a scope was written with: present and exact, or absent (an approval from before network policies).
|
|
362
|
+
const network = p["network"] === undefined ? undefined : networkFromValue(p["network"]);
|
|
363
|
+
if (network === null)
|
|
364
|
+
return null;
|
|
365
|
+
const withNetwork = network === undefined ? {} : { network };
|
|
335
366
|
if (p["provider"] === "claude") {
|
|
336
|
-
if (exactKeys(p, [...PROVIDER_KEYS, "permissionArgv"], ["timeoutKind"]) &&
|
|
367
|
+
if (exactKeys(p, [...PROVIDER_KEYS, "permissionArgv"], ["timeoutKind", "network"]) &&
|
|
337
368
|
str(p["model"]) &&
|
|
338
369
|
(p["permissionArgv"] === "auto" || p["permissionArgv"] === "acceptEdits" || p["permissionArgv"] === "bypassPermissions") &&
|
|
339
370
|
num(p["maxTurns"]) && num(p["repairMaxTurns"]) &&
|
|
@@ -349,12 +380,13 @@ export function profileFromJson(json) {
|
|
|
349
380
|
...(p["timeoutKind"] === "idle" ? { timeoutKind: "idle" } : {}),
|
|
350
381
|
repairTimeoutSeconds: p["repairTimeoutSeconds"],
|
|
351
382
|
repairModel: p["repairModel"],
|
|
383
|
+
...withNetwork,
|
|
352
384
|
};
|
|
353
385
|
}
|
|
354
386
|
return null;
|
|
355
387
|
}
|
|
356
388
|
if (p["provider"] === "codex" || p["provider"] === "openrouter") {
|
|
357
|
-
if (exactKeys(p, [...PROVIDER_KEYS, "sandboxMode"], ["timeoutKind"]) &&
|
|
389
|
+
if (exactKeys(p, [...PROVIDER_KEYS, "sandboxMode"], ["timeoutKind", "network"]) &&
|
|
358
390
|
str(p["model"]) &&
|
|
359
391
|
(p["sandboxMode"] === "workspace-write" || p["sandboxMode"] === "danger-full-access") &&
|
|
360
392
|
p["maxTurns"] === "unsupported" && p["repairMaxTurns"] === "unsupported" &&
|
|
@@ -370,12 +402,13 @@ export function profileFromJson(json) {
|
|
|
370
402
|
...(p["timeoutKind"] === "idle" ? { timeoutKind: "idle" } : {}),
|
|
371
403
|
repairTimeoutSeconds: p["repairTimeoutSeconds"],
|
|
372
404
|
repairModel: p["repairModel"],
|
|
405
|
+
...withNetwork,
|
|
373
406
|
};
|
|
374
407
|
}
|
|
375
408
|
return null;
|
|
376
409
|
}
|
|
377
410
|
if (p["provider"] === "gemini") {
|
|
378
|
-
if (exactKeys(p, [...PROVIDER_KEYS, "approvalArgv"], ["timeoutKind"]) &&
|
|
411
|
+
if (exactKeys(p, [...PROVIDER_KEYS, "approvalArgv"], ["timeoutKind", "network"]) &&
|
|
379
412
|
str(p["model"]) &&
|
|
380
413
|
(p["approvalArgv"] === "auto_edit" || p["approvalArgv"] === "yolo") &&
|
|
381
414
|
p["maxTurns"] === "unsupported" && p["repairMaxTurns"] === "unsupported" &&
|
|
@@ -391,6 +424,30 @@ export function profileFromJson(json) {
|
|
|
391
424
|
...(p["timeoutKind"] === "idle" ? { timeoutKind: "idle" } : {}),
|
|
392
425
|
repairTimeoutSeconds: p["repairTimeoutSeconds"],
|
|
393
426
|
repairModel: p["repairModel"],
|
|
427
|
+
...withNetwork,
|
|
428
|
+
};
|
|
429
|
+
}
|
|
430
|
+
return null;
|
|
431
|
+
}
|
|
432
|
+
if (typeof p["provider"] === "string" && isAcpProvider(p["provider"])) {
|
|
433
|
+
if (exactKeys(p, [...PROVIDER_KEYS, "permission", "idleTimeoutSeconds", "repairIdleTimeoutSeconds"], ["network"]) &&
|
|
434
|
+
str(p["model"]) &&
|
|
435
|
+
p["permission"] === "full" &&
|
|
436
|
+
p["maxTurns"] === "unsupported" && p["repairMaxTurns"] === "unsupported" &&
|
|
437
|
+
clock(p["timeoutSeconds"]) && clock(p["idleTimeoutSeconds"]) && clock(p["repairTimeoutSeconds"]) && clock(p["repairIdleTimeoutSeconds"]) &&
|
|
438
|
+
repairRef(p["repairModel"])) {
|
|
439
|
+
return {
|
|
440
|
+
provider: p["provider"],
|
|
441
|
+
model: p["model"],
|
|
442
|
+
permission: "full",
|
|
443
|
+
maxTurns: "unsupported",
|
|
444
|
+
repairMaxTurns: "unsupported",
|
|
445
|
+
timeoutSeconds: p["timeoutSeconds"],
|
|
446
|
+
idleTimeoutSeconds: p["idleTimeoutSeconds"],
|
|
447
|
+
repairTimeoutSeconds: p["repairTimeoutSeconds"],
|
|
448
|
+
repairIdleTimeoutSeconds: p["repairIdleTimeoutSeconds"],
|
|
449
|
+
repairModel: p["repairModel"],
|
|
450
|
+
...withNetwork,
|
|
394
451
|
};
|
|
395
452
|
}
|
|
396
453
|
return null;
|
|
@@ -678,7 +735,9 @@ password) {
|
|
|
678
735
|
// 1, finding 1): a viewer's or a revoked person's credential is real
|
|
679
736
|
// and still vouches for nothing — the CLI is a ceremony site like any
|
|
680
737
|
// other, and this was the one road that forgot.
|
|
681
|
-
|
|
738
|
+
// Creating an approver is a credential act (docs/plans/passwordless.md): a password typed now, never a session or
|
|
739
|
+
// the CLI's remembered login under Passwordless.
|
|
740
|
+
const vouching = by === undefined ? { ok: false } : proveCredential(store, by.name, by.token);
|
|
682
741
|
if (!vouching.ok) {
|
|
683
742
|
return { ok: false, reason: "not-an-approver" };
|
|
684
743
|
}
|
|
@@ -754,24 +813,129 @@ export function authenticateAccount(store, by, secret) {
|
|
|
754
813
|
return { ok: true, role: account.role, generation: account.generation };
|
|
755
814
|
}
|
|
756
815
|
/**
|
|
757
|
-
*
|
|
758
|
-
*
|
|
759
|
-
*
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
816
|
+
* The local CLI's remembered login, marked for the one command that read it (operate.ts credentialsFrom): with
|
|
817
|
+
* Passwordless on, a proof made with it records "cli-login" — never a typed password — and no credential act accepts it.
|
|
818
|
+
* Scoped to that command's async chain; a browser request in the console's own process never carries it.
|
|
819
|
+
*/
|
|
820
|
+
export const rememberedLogin = new AsyncLocalStorage();
|
|
821
|
+
export const FRESH_WINDOW_MS = 15 * 60_000;
|
|
822
|
+
/** How long an explicit provider re-check stands in for one password typed now. */
|
|
823
|
+
export const SSO_NOW_MS = 5 * 60_000;
|
|
824
|
+
export const freshFacts = new AsyncLocalStorage();
|
|
825
|
+
const refunds = new AsyncLocalStorage();
|
|
826
|
+
/**
|
|
827
|
+
* Run an act a re-check may prove, spending the re-check only if the act happens: when it throws (a rolled-back
|
|
828
|
+
* transaction) or `failed` says it was refused, every re-check it spent is put back. ONLY for acts whose every effect
|
|
829
|
+
* is in one database transaction (an approval, a chat approval proposal, a chat turn opening): there, "refused" and
|
|
830
|
+
* "thrown" prove nothing happened. Every other step-up spends its re-check whatever comes of it (fail-closed: at worst
|
|
831
|
+
* the person confirms with their provider again). Synchronous acts only.
|
|
832
|
+
*/
|
|
833
|
+
export function spendingOnlyOnSuccess(act, failed) {
|
|
834
|
+
const spent = [];
|
|
835
|
+
let result;
|
|
836
|
+
try {
|
|
837
|
+
result = refunds.run(spent, act);
|
|
838
|
+
}
|
|
839
|
+
catch (error) {
|
|
840
|
+
for (const refund of spent)
|
|
841
|
+
refund();
|
|
842
|
+
throw error;
|
|
843
|
+
}
|
|
844
|
+
if (failed(result))
|
|
845
|
+
for (const refund of spent)
|
|
846
|
+
refund();
|
|
847
|
+
// Succeeded here, but an enclosing act (an outer transaction, the request itself) may still fail: it owns them now.
|
|
848
|
+
else
|
|
849
|
+
refunds.getStore()?.push(...spent);
|
|
850
|
+
return result;
|
|
851
|
+
}
|
|
852
|
+
const freshScope = new AsyncLocalStorage();
|
|
853
|
+
/** Run an allowlisted act: within it, a fresh password or re-check may stand in for one typed now. */
|
|
854
|
+
export function withFreshness(op, act) { return freshScope.run(op, act); }
|
|
855
|
+
const within = (at, floor, spanMs, now) => at !== null && at > 0 && at > floor && at <= now + 60_000 && now - at < spanMs;
|
|
856
|
+
/** The freshness this act may stand on now, or null: only inside an allowlisted act on an allowlisted route, for this
|
|
857
|
+
* person, on a direct local request, on a live session, within the window, after any Forget. `preview` asks it from a
|
|
858
|
+
* page (a GET) about the allowlisted act its form posts to — for words only, never to authorise. */
|
|
859
|
+
export function freshnessFor(by, now = Date.now(), preview = false) {
|
|
860
|
+
const op = freshScope.getStore(), facts = freshFacts.getStore();
|
|
861
|
+
if (op === undefined || facts == null || facts.actor !== by || !(facts.allowlisted || preview) || !facts.local || !facts.live())
|
|
862
|
+
return null;
|
|
863
|
+
const floor = facts.floor(), password = facts.passwordAt(), sso = facts.ssoReauthAt();
|
|
864
|
+
if (within(password, floor, FRESH_WINDOW_MS, now))
|
|
865
|
+
return { method: "fresh-password", at: password };
|
|
866
|
+
if (within(sso, floor, FRESH_WINDOW_MS, now))
|
|
867
|
+
return { method: "fresh-sso", at: sso };
|
|
868
|
+
return null;
|
|
869
|
+
}
|
|
870
|
+
/** Whether an explicit provider re-check is waiting to stand in for one step-up (the page says "Confirmed with …"). */
|
|
871
|
+
export function ssoNowAvailable(by, now = Date.now()) {
|
|
872
|
+
const facts = freshFacts.getStore();
|
|
873
|
+
return facts != null && facts.actor === by && facts.live() && within(facts.ssoNow(), facts.floor(), SSO_NOW_MS, now);
|
|
874
|
+
}
|
|
875
|
+
/**
|
|
876
|
+
* Proof that this person is here now, for any account step-up (an approver's or not): a password typed now, or an
|
|
877
|
+
* explicit provider re-check (consumed). The core of every console step-up; `authenticateApprover` adds standing.
|
|
763
878
|
*/
|
|
764
|
-
export
|
|
765
|
-
|
|
766
|
-
|
|
879
|
+
export function proveAccountNow(store, by, token, options = {}) {
|
|
880
|
+
const now = Date.now();
|
|
881
|
+
const facts = freshFacts.getStore();
|
|
882
|
+
if (facts != null && facts.actor === by && !facts.live())
|
|
883
|
+
return { ok: false, reason: "not-an-approver" };
|
|
767
884
|
// An empty password is never checked as a password (and never counts toward a lockout).
|
|
768
|
-
if (token === "")
|
|
769
|
-
|
|
885
|
+
if (token === "") {
|
|
886
|
+
// Passwordless (read live): the person's own live browser session is the proof. Only a browser request carries facts;
|
|
887
|
+
// a CLI, a token, a chat bridge or detached work has none, and falls through to the rules below.
|
|
888
|
+
// A credential act never takes this road, so its own proofs (a one-use provider re-check) are reached and spent.
|
|
889
|
+
if (options.credential !== true && facts != null && facts.actor === by && facts.live() && passwordlessOn(store))
|
|
890
|
+
return { ok: true, method: "session", at: now };
|
|
891
|
+
if (ssoNowAvailable(by, now)) {
|
|
892
|
+
const at = facts.ssoNow();
|
|
893
|
+
const refund = facts.consumeSsoNow();
|
|
894
|
+
refunds.getStore()?.push(refund);
|
|
895
|
+
return { ok: true, method: "sso-now", at };
|
|
896
|
+
}
|
|
897
|
+
const fresh = freshnessFor(by, now);
|
|
898
|
+
return fresh === null ? { ok: false, reason: "not-an-approver" } : { ok: true, method: fresh.method, at: fresh.at };
|
|
899
|
+
}
|
|
770
900
|
const account = authenticateAccount(store, by, token);
|
|
771
|
-
if (!account.ok)
|
|
901
|
+
if (!account.ok)
|
|
772
902
|
return { ok: false, reason: account.reason === "no-approvers" ? "no-approvers" : "not-an-approver" };
|
|
773
|
-
|
|
774
|
-
|
|
903
|
+
// A machine credential named the person (authenticateAccount takes it as them): say so, never "a password typed now".
|
|
904
|
+
if (activeRemote() !== null)
|
|
905
|
+
return { ok: true, method: "remote-token", at: now };
|
|
906
|
+
if (currentActor()?.lead === true && parseLeadToken(token) !== null)
|
|
907
|
+
return { ok: true, method: "lead-token", at: now };
|
|
908
|
+
const remembered = rememberedLogin.getStore()?.login;
|
|
909
|
+
if (remembered != null && remembered.name === by && remembered.token === token && passwordlessOn(store))
|
|
910
|
+
return { ok: true, method: "cli-login", at: now };
|
|
911
|
+
if (facts != null && facts.actor === by)
|
|
912
|
+
facts.stamp(now);
|
|
913
|
+
return { ok: true, method: "password-now", at: now };
|
|
914
|
+
}
|
|
915
|
+
/**
|
|
916
|
+
* A credential act (docs/plans/passwordless.md, the closed list: one that creates or widens a way in): only a password
|
|
917
|
+
* typed now, or a one-use identity-provider re-check, proves it — never a session, never a remembered password.
|
|
918
|
+
*/
|
|
919
|
+
/** Whether an empty password would pass for `by` right now because Passwordless is on and this is their live browser
|
|
920
|
+
* session. Read only — the act itself proves it again inside `proveAccountNow`. */
|
|
921
|
+
export function passwordlessSessionFor(store, by) {
|
|
922
|
+
const facts = freshFacts.getStore();
|
|
923
|
+
return facts != null && facts.actor === by && facts.live() && passwordlessOn(store);
|
|
924
|
+
}
|
|
925
|
+
/** Whether a proof is one a credential act accepts: a password typed now, or a one-use identity-provider re-check. */
|
|
926
|
+
export const credentialProof = (method) => method === "password-now" || method === "sso-now";
|
|
927
|
+
export function proveCredential(store, by, token, repo) {
|
|
928
|
+
const proved = authenticateApprover(store, by, token, repo, { credential: true });
|
|
929
|
+
if (!proved.ok)
|
|
930
|
+
return proved;
|
|
931
|
+
return credentialProof(proved.method) ? proved : { ok: false, reason: "not-an-approver" };
|
|
932
|
+
}
|
|
933
|
+
export function authenticateApprover(store, by, token, repo, options = {}) {
|
|
934
|
+
const proved = proveAccountNow(store, by, token, options);
|
|
935
|
+
if (!proved.ok)
|
|
936
|
+
return proved;
|
|
937
|
+
const standing = approverStanding(store, by, repo);
|
|
938
|
+
return standing.ok ? proved : standing;
|
|
775
939
|
}
|
|
776
940
|
/**
|
|
777
941
|
* What an approval needs besides the password (v100: an identity-provider
|
|
@@ -910,25 +1074,79 @@ export function approve(store, taskId, by, now, sawDigest, token, mutation = {})
|
|
|
910
1074
|
const gate = store.approvalGate(taskId, by, "person");
|
|
911
1075
|
if (gate.verdict === "refuse")
|
|
912
1076
|
return { ok: false, reason: gate.reason };
|
|
1077
|
+
// A remembered password (cockpit B2) approves only everyday work: one approver, standard permissions, terms a
|
|
1078
|
+
// person filed. Protected work, escalated terms and quarantined filings take a password typed now.
|
|
1079
|
+
if ((authenticated.method === "fresh-password" || authenticated.method === "fresh-sso") && !freshlyApprovable(store, taskId, scope, gate.verdict === "seal" && !gate.protectedWork)) {
|
|
1080
|
+
return { ok: false, reason: "password-now" };
|
|
1081
|
+
}
|
|
1082
|
+
// How the person was proved, beside every yes they give (a vote kept for protected work, or the seal): a password
|
|
1083
|
+
// typed now, a re-check now, or a remembered one — with when, the credential generation and the exact digest.
|
|
1084
|
+
const proved = () => store.recordAction({ at: now.toISOString(), actor: by, repo: store.lookupRef(taskId)?.repo ?? null, taskId, runId: null, action: APPROVAL_PROOF,
|
|
1085
|
+
outcome: authenticated.method, source: "work", detail: approvalProofDetail(authenticated.at, store.accountOf(by)?.generation ?? 0, scope.digest) });
|
|
913
1086
|
if (gate.verdict === "vote") {
|
|
1087
|
+
// The same person again adds nothing: no new vote, no proof row (and the approval roads refund a re-check).
|
|
1088
|
+
if (gate.already)
|
|
1089
|
+
return { ok: false, reason: "second-approver", have: gate.have, need: gate.need, already: true };
|
|
914
1090
|
store.recordApprovalVote(taskId, scope.digest, by, now);
|
|
1091
|
+
proved();
|
|
915
1092
|
return { ok: false, reason: "second-approver", have: gate.have, need: gate.need, already: gate.already };
|
|
916
1093
|
}
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
1094
|
+
// The vote, the seal and its proof stand or fall together: a seal that is refused takes this yes back with it
|
|
1095
|
+
// (savepoint rolled back), so a refusal leaves nothing written — the approval roads may then refund a re-check.
|
|
1096
|
+
const refused = {};
|
|
1097
|
+
try {
|
|
1098
|
+
return store.savepoint(() => {
|
|
1099
|
+
if (gate.protectedWork)
|
|
1100
|
+
store.recordApprovalVote(taskId, scope.digest, by, now);
|
|
1101
|
+
// SEAL, never re-resolve: the approval snapshots the stored working
|
|
1102
|
+
// profile — the exact bytes the digest the approver signed was bound
|
|
1103
|
+
// to. Routing saveScope here would re-run resolution and could sign a
|
|
1104
|
+
// profile nobody saw.
|
|
1105
|
+
const sealed = store.sealScopeApproval(taskId, by, now, mutation);
|
|
1106
|
+
const approved = sealed ? store.getScope(taskId) : null;
|
|
1107
|
+
if (!sealed || approved === null) {
|
|
1108
|
+
refused.result = { ok: false, reason: sealed ? "no-scope" : "changed" };
|
|
1109
|
+
throw refused;
|
|
1110
|
+
}
|
|
1111
|
+
proved();
|
|
1112
|
+
return { ok: true, scope: approved };
|
|
1113
|
+
});
|
|
1114
|
+
}
|
|
1115
|
+
catch (error) {
|
|
1116
|
+
if (error === refused && refused.result !== undefined)
|
|
1117
|
+
return refused.result;
|
|
1118
|
+
throw error;
|
|
1119
|
+
}
|
|
930
1120
|
});
|
|
931
1121
|
}
|
|
1122
|
+
/** The ledger act that records how an approval's person was proved (password-now, sso-now, fresh-password, fresh-sso). */
|
|
1123
|
+
export const APPROVAL_PROOF = "approval proved";
|
|
1124
|
+
/** The proof row's detail: when the person was proved, the generation, and the exact digest they said yes to. */
|
|
1125
|
+
export const approvalProofDetail = (at, generation, digest) => `${new Date(at).toISOString()} · generation ${generation} · digest ${digest}`;
|
|
1126
|
+
/** The proof of THIS approval: the approver's own row for these exact bytes (never another approval's words). */
|
|
1127
|
+
export function approvalProofOf(store, taskId, by, digest) {
|
|
1128
|
+
const rows = store.handle.prepare("SELECT outcome, detail FROM action_ledger WHERE task_id = ? AND action = ? AND actor = ? ORDER BY id DESC LIMIT 50").all(taskId, APPROVAL_PROOF, by);
|
|
1129
|
+
for (const row of rows) {
|
|
1130
|
+
const parts = typeof row["detail"] === "string" ? row["detail"].split(" · ") : [];
|
|
1131
|
+
if (parts.length !== 3 || parts[2] !== `digest ${digest}` || Number.isNaN(Date.parse(parts[0])))
|
|
1132
|
+
continue;
|
|
1133
|
+
return { method: String(row["outcome"]), at: parts[0] };
|
|
1134
|
+
}
|
|
1135
|
+
return null;
|
|
1136
|
+
}
|
|
1137
|
+
/** Whether a remembered password may approve these terms: one approver (no protected work), standard permissions
|
|
1138
|
+
* (never escalated), and not filed by a coordinator, a lead or a scout (those keep their own password ceremony). */
|
|
1139
|
+
export function freshlyApprovable(store, taskId, scope, singleApprover) {
|
|
1140
|
+
if (!singleApprover)
|
|
1141
|
+
return false;
|
|
1142
|
+
if (scope.profile == null || levelOfProfile(scope.profile) === "escalated")
|
|
1143
|
+
return false;
|
|
1144
|
+
const ref = store.handle.prepare("SELECT coordinator_cid FROM task_ref WHERE backend = 'built-in' AND external_id = ?").get(taskId);
|
|
1145
|
+
if (ref === undefined || ref["coordinator_cid"] != null)
|
|
1146
|
+
return false;
|
|
1147
|
+
const via = store.handle.prepare("SELECT proposed_via FROM task_scope WHERE task_id = ?").get(taskId)?.["proposed_via"];
|
|
1148
|
+
return !(via === "mate" || via === "coordinator" || via === "scout");
|
|
1149
|
+
}
|
|
932
1150
|
/**
|
|
933
1151
|
* Whether this task may be built.
|
|
934
1152
|
*
|
|
@@ -1058,6 +1276,8 @@ export function describeScope(scope, readiness = NO_READINESS) {
|
|
|
1058
1276
|
// The ROUTE, in the words the yes agrees to (v47): posture and every
|
|
1059
1277
|
// leg with its reason and readiness — before anyone signs.
|
|
1060
1278
|
...scopeRouteWords(scope, readiness),
|
|
1279
|
+
// What the build may reach (egress.ts), part of what the yes agrees to.
|
|
1280
|
+
...(scope.profile === null || scope.profile === undefined ? [] : [` network ${networkWords(scope.profile.network ?? null).replace(/^Network: /, "")}`]),
|
|
1061
1281
|
` reference ${scope.digest}`,
|
|
1062
1282
|
` approved ${describeApproval(approval)}`,
|
|
1063
1283
|
];
|
package/dist/scout-net.d.ts
CHANGED
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
import { type Socket } from "node:net";
|
|
2
|
-
/**
|
|
2
|
+
/**
|
|
3
|
+
* Whether an IP literal is a public internet address. IPv4 inside IPv6 (mapped, translated, compatible or NAT64) is
|
|
4
|
+
* judged as IPv4, read from the address's groups so every way of writing it is caught (an expanded NAT64 form too).
|
|
5
|
+
*/
|
|
3
6
|
export declare function isPublicAddress(address: string): boolean;
|
|
4
7
|
export type Lookup = (host: string) => Promise<readonly string[]>;
|
|
5
8
|
/**
|