toolroll 0.9.62 → 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 +80 -0
- package/README.md +66 -8
- 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 +203 -10
- package/dist/agent-fence.js +623 -41
- 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 +263 -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 +199 -17
- 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/check-levels.js +3 -0
- 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 +20 -1
- package/dist/cli-help.d.ts +1 -1
- package/dist/cli-help.js +5 -1
- package/dist/cli.d.ts +3 -0
- package/dist/cli.js +24 -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/cli.d.ts +8 -0
- package/dist/contracts/cli.js +3 -1
- package/dist/contracts/console-api.d.ts +30 -4
- package/dist/contracts/console-api.js +13 -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/integration-metadata.d.ts +2 -1
- package/dist/contracts/integration-metadata.js +2 -1
- 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/discord-chat.js +4 -1
- package/dist/dispatch.js +7 -4
- package/dist/doctor.d.ts +53 -0
- package/dist/doctor.js +427 -0
- 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/evidence-pack.d.ts +1 -1
- package/dist/evidence-pack.js +2 -2
- package/dist/exec.d.ts +81 -0
- package/dist/exec.js +368 -9
- package/dist/export-ui.js +2 -2
- package/dist/flow-code.d.ts +2 -2
- package/dist/flow-code.js +4 -15
- package/dist/flow-engine.js +35 -11
- 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 +84 -11
- package/dist/flow-triggers.js +554 -87
- package/dist/flow-unattended-words.d.ts +28 -0
- package/dist/flow-unattended-words.js +50 -0
- package/dist/flow-unattended.d.ts +227 -0
- package/dist/flow-unattended.js +776 -0
- package/dist/flows-cli.d.ts +28 -1
- package/dist/flows-cli.js +63 -12
- package/dist/flows-ui.d.ts +2 -3
- package/dist/flows-ui.js +56 -5
- package/dist/flows.d.ts +4 -0
- package/dist/flows.js +9 -1
- package/dist/google-mail.d.ts +1 -0
- package/dist/google-mail.js +61 -18
- package/dist/html.d.ts +9 -0
- package/dist/html.js +11 -0
- package/dist/integrations-ui.d.ts +2 -1
- package/dist/integrations-ui.js +6 -3
- package/dist/integrations.d.ts +17 -3
- package/dist/integrations.js +38 -18
- package/dist/invoke.d.ts +17 -0
- package/dist/invoke.js +510 -182
- package/dist/keys.d.ts +18 -14
- package/dist/keys.js +5 -0
- package/dist/landed-cleanup.d.ts +3 -2
- package/dist/landed-cleanup.js +12 -5
- 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 +163 -4
- 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-connect.d.ts +8 -7
- package/dist/mcp-connect.js +49 -51
- 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/oauth-run-token.d.ts +51 -0
- package/dist/oauth-run-token.js +127 -0
- package/dist/operate.d.ts +2 -0
- package/dist/operate.js +282 -18
- 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/plan-auto.d.ts +11 -0
- package/dist/plan-auto.js +62 -11
- 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/process-recovery-eligibility.d.ts +1 -1
- package/dist/project-access.d.ts +3 -0
- package/dist/project-access.js +3 -0
- package/dist/project-delete-ui.d.ts +12 -0
- package/dist/project-delete-ui.js +41 -9
- package/dist/project-delete.d.ts +3 -1
- package/dist/project-delete.js +12 -6
- package/dist/project-file.d.ts +3 -0
- package/dist/project-file.js +16 -0
- package/dist/project-memory.d.ts +3 -1
- package/dist/project-memory.js +57 -25
- package/dist/project-tools.d.ts +103 -27
- package/dist/project-tools.js +399 -100
- 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-home.d.ts +22 -0
- package/dist/provider-home.js +99 -0
- package/dist/provider.d.ts +42 -5
- package/dist/provider.js +97 -19
- 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/remote-main.d.ts +4 -0
- package/dist/remote-main.js +3 -3
- 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 +171 -6
- 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 +258 -43
- 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 +65 -13
- 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 +49 -5
- package/dist/server/runtime.d.ts +34 -3
- package/dist/server/session.d.ts +25 -1
- package/dist/server/session.js +18 -4
- package/dist/server/settings.js +132 -13
- 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-sweep-child.js +3 -1
- package/dist/storage-sweep-process.d.ts +12 -5
- package/dist/storage-sweep-process.js +23 -6
- 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 +466 -19
- package/dist/store.js +1185 -63
- package/dist/subagent-desk.js +1 -1
- package/dist/subagent-tools.d.ts +1 -1
- package/dist/summary.js +3 -0
- package/dist/surface.js +6 -4
- 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/teardown-git.d.ts +14 -0
- package/dist/teardown-git.js +119 -0
- 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 +39 -0
- package/dist/tool-broker.js +370 -0
- package/dist/tool-launcher.js +5 -4
- package/dist/tool-secret-store.d.ts +170 -0
- package/dist/tool-secret-store.js +834 -0
- package/dist/toolroll-update-ui.js +2 -2
- package/dist/toolroll-update.js +14 -3
- package/dist/tools-ui.d.ts +7 -1
- package/dist/tools-ui.js +19 -10
- 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 +21 -5
- package/dist/workspace-revision.js +3 -1
- package/dist/worktree.d.ts +10 -4
- package/dist/worktree.js +70 -29
- package/package.json +13 -2
- package/scripts/provider-canary.mjs +210 -31
package/dist/egress.d.ts
ADDED
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
import { type Lookup } from "./scout-net.js";
|
|
2
|
+
/** The policy's shape version: a worker enforces the versions it knows and refuses any other. */
|
|
3
|
+
export declare const NETWORK_POLICY_VERSION = 1;
|
|
4
|
+
/** `allowlist`: only the snapshot's hosts, through the run's proxy. `open`: anywhere (an approver's explicit choice). */
|
|
5
|
+
export type NetworkMode = "allowlist" | "open";
|
|
6
|
+
/**
|
|
7
|
+
* What a build may reach on this machine's own loopback. Linux gives every build a loopback of its own, so this only
|
|
8
|
+
* matters on macOS, where one loopback serves everything and the sandbox can't admit some local ports while refusing
|
|
9
|
+
* others: `proxy-only` (the default) admits nothing local but the run's own proxy and tool broker; `any` admits every
|
|
10
|
+
* local service on this Mac — local test servers, and also anything else listening here — an approver's explicit choice.
|
|
11
|
+
*/
|
|
12
|
+
export type LoopbackMode = "any" | "proxy-only";
|
|
13
|
+
/** The network a scope was approved with (sealed in its execution profile). */
|
|
14
|
+
export type NetworkProfile = {
|
|
15
|
+
version: 1;
|
|
16
|
+
mode: NetworkMode;
|
|
17
|
+
hosts: string[];
|
|
18
|
+
loopback: LoopbackMode;
|
|
19
|
+
};
|
|
20
|
+
/** A project's own network setting (an approver's): what new scopes of it are written with. */
|
|
21
|
+
export type ProjectNetwork = {
|
|
22
|
+
mode: NetworkMode;
|
|
23
|
+
hosts: string[];
|
|
24
|
+
loopback: LoopbackMode;
|
|
25
|
+
};
|
|
26
|
+
export declare const PROJECT_NETWORK_DEFAULT: ProjectNetwork;
|
|
27
|
+
/** At most this many hosts a project adds (a list longer than this is a different policy: open). */
|
|
28
|
+
export declare const MAX_NETWORK_HOSTS = 200;
|
|
29
|
+
/** At most this many in a sealed snapshot: the project's, plus its provider's and every registry's. */
|
|
30
|
+
export declare const MAX_SNAPSHOT_HOSTS: number;
|
|
31
|
+
/**
|
|
32
|
+
* A host as a policy names it: lowercase ASCII (an international name in its punycode form), no trailing dot, at
|
|
33
|
+
* least two labels, each 1–63 letters, digits or hyphens; or `*.` and such a name, for every name under it. Never an
|
|
34
|
+
* IP address, a port, a path or a bare top-level name. Null when it is none of these.
|
|
35
|
+
*/
|
|
36
|
+
export declare function normalizeHost(input: string): string | null;
|
|
37
|
+
/** Whether `host` (as a client named it) is one a policy's list admits: an exact name, or a name under a `*.` entry. */
|
|
38
|
+
export declare function hostAllowed(host: string, hosts: readonly string[]): boolean;
|
|
39
|
+
/**
|
|
40
|
+
* Each provider's own hosts (catalog version 1): its model API and the sign-in refresh its CLI needs. Codex starts
|
|
41
|
+
* its harness outside its command sandbox, so its hosts matter only to what its commands may reach. An ACP agent
|
|
42
|
+
* talks to whatever model service it was set up with; its row names the hosts its own service uses.
|
|
43
|
+
*/
|
|
44
|
+
export declare const PROVIDER_HOSTS: Readonly<Record<string, readonly string[]>>;
|
|
45
|
+
/** A package ecosystem a project's files name, and the registry hosts its installs read from. */
|
|
46
|
+
export type Ecosystem = "npm" | "python" | "rust" | "go" | "ruby" | "jvm";
|
|
47
|
+
export declare const REGISTRY_HOSTS: Readonly<Record<Ecosystem, readonly string[]>>;
|
|
48
|
+
/** The ecosystems a list of repository paths names (requirements-*.txt counts as Python). */
|
|
49
|
+
export declare function ecosystemsOf(paths: readonly string[]): Ecosystem[];
|
|
50
|
+
/**
|
|
51
|
+
* The ecosystems the project's own checkout names: its files a few folders deep, read straight from the folder the
|
|
52
|
+
* plane registered (which no build can write: agent-fence.ts taskBoundary), never a build's worktree. No process is
|
|
53
|
+
* started and nothing in the repository runs. An unreadable folder names none: its builds reach the provider and the
|
|
54
|
+
* project's own hosts only.
|
|
55
|
+
*/
|
|
56
|
+
export declare function repositoryEcosystems(repo: string): Ecosystem[];
|
|
57
|
+
/** The network a new scope is written with: the project's setting, its provider's hosts and its ecosystems' registries. */
|
|
58
|
+
export declare function resolveNetwork(provider: string, project: ProjectNetwork, ecosystems: readonly Ecosystem[]): NetworkProfile;
|
|
59
|
+
/** The network of a launch nothing sealed one for (a planner, a check): its provider's own hosts and nothing else. */
|
|
60
|
+
export declare function providerOnlyNetwork(provider: string): NetworkProfile;
|
|
61
|
+
/**
|
|
62
|
+
* The network an approval sealed before network policies existed runs under: a deliberate narrowing (provider hosts
|
|
63
|
+
* and the repository's registries, nothing the project added since, local services of this Mac out of reach). Never
|
|
64
|
+
* widened by a setting changed after it was approved.
|
|
65
|
+
*/
|
|
66
|
+
export declare function legacyNetwork(provider: string, ecosystems: readonly Ecosystem[]): NetworkProfile;
|
|
67
|
+
/** Strict re-hydration (scope.ts profileFromJson): exactly the keys this code writes, every host normalized, or null. */
|
|
68
|
+
export declare function networkFromValue(value: unknown): NetworkProfile | null;
|
|
69
|
+
/** A project setting as saved (store.ts project_network), or null when it isn't one this code wrote. */
|
|
70
|
+
export declare function projectNetworkFromValue(value: unknown): ProjectNetwork | null;
|
|
71
|
+
/** The network in plain words, for an approval card and a run's record. */
|
|
72
|
+
export declare function networkWords(network: NetworkProfile | null): string;
|
|
73
|
+
/** What the run's proxy saw: connections it made and refused, by host (at most HOST_LIMIT hosts, the rest summed). */
|
|
74
|
+
export type EgressCounts = {
|
|
75
|
+
connections: number;
|
|
76
|
+
refused: number;
|
|
77
|
+
hosts: {
|
|
78
|
+
host: string;
|
|
79
|
+
connections: number;
|
|
80
|
+
refused: number;
|
|
81
|
+
}[];
|
|
82
|
+
otherHosts: number;
|
|
83
|
+
};
|
|
84
|
+
/** What a run's record keeps about its network: the mode it ran under, whether a caller's own proxy carried it, how many
|
|
85
|
+
* hosts its list held, and what the run's proxy saw. */
|
|
86
|
+
export type RunNetworkRecord = {
|
|
87
|
+
mode: "open" | "allowlist";
|
|
88
|
+
phase: "agent";
|
|
89
|
+
via?: true;
|
|
90
|
+
hosts?: number;
|
|
91
|
+
loopback?: LoopbackMode;
|
|
92
|
+
counts?: EgressCounts;
|
|
93
|
+
};
|
|
94
|
+
/** A stored record read back strictly: anything this code didn't write reads as none. */
|
|
95
|
+
export declare function runNetworkFromValue(value: unknown): RunNetworkRecord | null;
|
|
96
|
+
export type EgressProxy = {
|
|
97
|
+
/** The proxy's address with this run's credentials, for HTTPS_PROXY and the like. */
|
|
98
|
+
url: string;
|
|
99
|
+
port: number;
|
|
100
|
+
counts: () => EgressCounts;
|
|
101
|
+
close: () => Promise<void>;
|
|
102
|
+
};
|
|
103
|
+
/** Every proxy setting a program might read, both cases, so none falls back to a direct connection the OS refuses. */
|
|
104
|
+
export declare function proxyEnv(proxy: EgressProxy, loopback: LoopbackMode): Record<string, string>;
|
|
105
|
+
/**
|
|
106
|
+
* Start one run's proxy on loopback. Only a client presenting this run's credentials is served (another run's agent
|
|
107
|
+
* on the same machine can't borrow it). CONNECT goes only to port 443 and plain HTTP only to port 80, only to a host
|
|
108
|
+
* the run's list admits (matched by name before it is resolved), only to public addresses (the proxy resolves the
|
|
109
|
+
* name itself and connects to exactly the addresses it checked). A plain HTTP request's Host must be the URL's host.
|
|
110
|
+
*/
|
|
111
|
+
export declare function startEgressProxy(options: {
|
|
112
|
+
hosts: readonly string[];
|
|
113
|
+
lookup?: Lookup;
|
|
114
|
+
decided?: (host: string, allowed: boolean, why: string) => void;
|
|
115
|
+
/** Tests only: which resolved addresses may be reached (default: public internet addresses), and the port an
|
|
116
|
+
* approved request is relayed to (default: the one it asked for). */
|
|
117
|
+
addressAllowed?: (address: string) => boolean;
|
|
118
|
+
upstreamPort?: (port: number) => number;
|
|
119
|
+
}): Promise<EgressProxy>;
|
package/dist/egress.js
ADDED
|
@@ -0,0 +1,423 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What a build may reach on the network (docs/plans/agent-baseline-hardening.md, arc 1).
|
|
3
|
+
*
|
|
4
|
+
* A run's network is part of its sealed execution profile: the hosts it may reach are resolved once, when the scope
|
|
5
|
+
* is written (the provider's own hosts, the package registries the project's own checkout names,
|
|
6
|
+
* and the project's added hosts), and that snapshot is what the approval binds and the launch enforces. Nothing at
|
|
7
|
+
* launch re-derives it from settings changed since or from files the agent wrote.
|
|
8
|
+
*
|
|
9
|
+
* The launch enforces it at the operating system (agent-fence.ts: Seatbelt's network rules on macOS, a network
|
|
10
|
+
* namespace on Linux) and reaches the world through one proxy of its own (startEgressProxy), which admits only the
|
|
11
|
+
* snapshot's hosts. The proxy never decrypts TLS: it restricts where a build connects, not what it sends there.
|
|
12
|
+
*/
|
|
13
|
+
import { createServer, request as httpRequest } from "node:http";
|
|
14
|
+
import { randomBytes, timingSafeEqual } from "node:crypto";
|
|
15
|
+
import { isIP } from "node:net";
|
|
16
|
+
import { domainToASCII } from "node:url";
|
|
17
|
+
import { readdirSync } from "node:fs";
|
|
18
|
+
import { basename, join } from "node:path";
|
|
19
|
+
import { connectFirst, isPublicAddress } from "./scout-net.js";
|
|
20
|
+
import { lookup as dnsLookup } from "node:dns/promises";
|
|
21
|
+
/** The policy's shape version: a worker enforces the versions it knows and refuses any other. */
|
|
22
|
+
export const NETWORK_POLICY_VERSION = 1;
|
|
23
|
+
export const PROJECT_NETWORK_DEFAULT = { mode: "allowlist", hosts: [], loopback: "proxy-only" };
|
|
24
|
+
/** At most this many hosts a project adds (a list longer than this is a different policy: open). */
|
|
25
|
+
export const MAX_NETWORK_HOSTS = 200;
|
|
26
|
+
/** At most this many in a sealed snapshot: the project's, plus its provider's and every registry's. */
|
|
27
|
+
export const MAX_SNAPSHOT_HOSTS = MAX_NETWORK_HOSTS + 100;
|
|
28
|
+
/**
|
|
29
|
+
* A host as a policy names it: lowercase ASCII (an international name in its punycode form), no trailing dot, at
|
|
30
|
+
* least two labels, each 1–63 letters, digits or hyphens; or `*.` and such a name, for every name under it. Never an
|
|
31
|
+
* IP address, a port, a path or a bare top-level name. Null when it is none of these.
|
|
32
|
+
*/
|
|
33
|
+
export function normalizeHost(input) {
|
|
34
|
+
const trimmed = input.trim().toLowerCase().replace(/\.$/, "");
|
|
35
|
+
const wildcard = trimmed.startsWith("*.");
|
|
36
|
+
const bare = wildcard ? trimmed.slice(2) : trimmed;
|
|
37
|
+
if (bare === "" || bare.length > 253 || isIP(bare.replace(/^\[|\]$/g, "")) !== 0)
|
|
38
|
+
return null;
|
|
39
|
+
// Nothing of a URL's other parts: the converter below would quietly cut a name at a slash or a colon.
|
|
40
|
+
if (/[\s/\\:@?#%[\]*,;]/.test(bare))
|
|
41
|
+
return null;
|
|
42
|
+
const ascii = domainToASCII(bare);
|
|
43
|
+
if (ascii === "" || ascii !== ascii.toLowerCase())
|
|
44
|
+
return null;
|
|
45
|
+
const labels = ascii.split(".");
|
|
46
|
+
if (labels.length < 2)
|
|
47
|
+
return null;
|
|
48
|
+
if (!labels.every(label => /^[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?$/.test(label)))
|
|
49
|
+
return null;
|
|
50
|
+
// A name whose last label is all digits reads as an address to too many resolvers.
|
|
51
|
+
if (/^\d+$/.test(labels.at(-1)))
|
|
52
|
+
return null;
|
|
53
|
+
return wildcard ? `*.${ascii}` : ascii;
|
|
54
|
+
}
|
|
55
|
+
/** Whether `host` (as a client named it) is one a policy's list admits: an exact name, or a name under a `*.` entry. */
|
|
56
|
+
export function hostAllowed(host, hosts) {
|
|
57
|
+
const name = normalizeHost(host);
|
|
58
|
+
if (name === null || name.startsWith("*."))
|
|
59
|
+
return false;
|
|
60
|
+
return hosts.some(entry => entry === name || (entry.startsWith("*.") && name.endsWith(entry.slice(1))));
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* Each provider's own hosts (catalog version 1): its model API and the sign-in refresh its CLI needs. Codex starts
|
|
64
|
+
* its harness outside its command sandbox, so its hosts matter only to what its commands may reach. An ACP agent
|
|
65
|
+
* talks to whatever model service it was set up with; its row names the hosts its own service uses.
|
|
66
|
+
*/
|
|
67
|
+
export const PROVIDER_HOSTS = {
|
|
68
|
+
claude: ["api.anthropic.com", "console.anthropic.com", "platform.claude.com"],
|
|
69
|
+
codex: ["api.openai.com", "chatgpt.com", "auth.openai.com"],
|
|
70
|
+
openrouter: ["openrouter.ai"],
|
|
71
|
+
gemini: ["generativelanguage.googleapis.com", "cloudcode-pa.googleapis.com", "oauth2.googleapis.com"],
|
|
72
|
+
opencode: ["opencode.ai", "*.opencode.ai", "models.dev"],
|
|
73
|
+
cursor: ["*.cursor.sh", "*.cursor.com"],
|
|
74
|
+
cline: ["api.cline.bot"],
|
|
75
|
+
qwen: ["chat.qwen.ai", "dashscope.aliyuncs.com", "dashscope-intl.aliyuncs.com"],
|
|
76
|
+
// Goose talks to whatever model service it is set up with: a project using it adds that service's host.
|
|
77
|
+
goose: [],
|
|
78
|
+
};
|
|
79
|
+
export const REGISTRY_HOSTS = {
|
|
80
|
+
npm: ["registry.npmjs.org", "registry.yarnpkg.com", "repo.yarnpkg.com", "nodejs.org"],
|
|
81
|
+
python: ["pypi.org", "files.pythonhosted.org"],
|
|
82
|
+
rust: ["crates.io", "index.crates.io", "static.crates.io"],
|
|
83
|
+
go: ["proxy.golang.org", "sum.golang.org"],
|
|
84
|
+
ruby: ["rubygems.org", "index.rubygems.org"],
|
|
85
|
+
jvm: ["repo.maven.apache.org", "repo1.maven.org", "plugins.gradle.org", "services.gradle.org", "downloads.gradle.org"],
|
|
86
|
+
};
|
|
87
|
+
/** The files that name each ecosystem, by file name wherever they are in the tree. */
|
|
88
|
+
const ECOSYSTEM_FILES = {
|
|
89
|
+
"package.json": "npm", "package-lock.json": "npm", "npm-shrinkwrap.json": "npm", "yarn.lock": "npm", "pnpm-lock.yaml": "npm", "bun.lock": "npm", "bun.lockb": "npm",
|
|
90
|
+
"pyproject.toml": "python", "requirements.txt": "python", "pipfile": "python", "pipfile.lock": "python", "poetry.lock": "python", "uv.lock": "python", "setup.py": "python", "setup.cfg": "python",
|
|
91
|
+
"cargo.toml": "rust", "cargo.lock": "rust",
|
|
92
|
+
"go.mod": "go", "go.sum": "go",
|
|
93
|
+
"gemfile": "ruby", "gemfile.lock": "ruby",
|
|
94
|
+
"pom.xml": "jvm", "build.gradle": "jvm", "build.gradle.kts": "jvm", "settings.gradle": "jvm", "settings.gradle.kts": "jvm",
|
|
95
|
+
};
|
|
96
|
+
/** The ecosystems a list of repository paths names (requirements-*.txt counts as Python). */
|
|
97
|
+
export function ecosystemsOf(paths) {
|
|
98
|
+
const found = new Set();
|
|
99
|
+
for (const path of paths) {
|
|
100
|
+
const name = basename(path).toLowerCase();
|
|
101
|
+
const one = ECOSYSTEM_FILES[name] ?? (/^requirements[\w.-]*\.txt$/.test(name) ? "python" : undefined);
|
|
102
|
+
if (one !== undefined)
|
|
103
|
+
found.add(one);
|
|
104
|
+
}
|
|
105
|
+
return [...found].sort();
|
|
106
|
+
}
|
|
107
|
+
/** How deep and how wide the walk for ecosystem files goes (a monorepo names its ecosystems near its root). */
|
|
108
|
+
const WALK_DEPTH = 3, WALK_LIMIT = 5_000;
|
|
109
|
+
const WALK_SKIP = new Set(["node_modules", "vendor", "target", "dist", "build", "venv", "__pycache__"]);
|
|
110
|
+
/**
|
|
111
|
+
* The ecosystems the project's own checkout names: its files a few folders deep, read straight from the folder the
|
|
112
|
+
* plane registered (which no build can write: agent-fence.ts taskBoundary), never a build's worktree. No process is
|
|
113
|
+
* started and nothing in the repository runs. An unreadable folder names none: its builds reach the provider and the
|
|
114
|
+
* project's own hosts only.
|
|
115
|
+
*/
|
|
116
|
+
export function repositoryEcosystems(repo) {
|
|
117
|
+
const paths = [];
|
|
118
|
+
const walk = (folder, depth) => {
|
|
119
|
+
let entries;
|
|
120
|
+
try {
|
|
121
|
+
entries = readdirSync(folder, { withFileTypes: true });
|
|
122
|
+
}
|
|
123
|
+
catch {
|
|
124
|
+
return;
|
|
125
|
+
}
|
|
126
|
+
for (const entry of entries) {
|
|
127
|
+
if (paths.length >= WALK_LIMIT)
|
|
128
|
+
return;
|
|
129
|
+
if (entry.isFile())
|
|
130
|
+
paths.push(join(folder, entry.name));
|
|
131
|
+
else if (entry.isDirectory() && depth < WALK_DEPTH && !entry.name.startsWith(".") && !WALK_SKIP.has(entry.name))
|
|
132
|
+
walk(join(folder, entry.name), depth + 1);
|
|
133
|
+
}
|
|
134
|
+
};
|
|
135
|
+
walk(repo, 0);
|
|
136
|
+
return ecosystemsOf(paths);
|
|
137
|
+
}
|
|
138
|
+
/** The network a new scope is written with: the project's setting, its provider's hosts and its ecosystems' registries. */
|
|
139
|
+
export function resolveNetwork(provider, project, ecosystems) {
|
|
140
|
+
if (project.mode === "open")
|
|
141
|
+
return { version: NETWORK_POLICY_VERSION, mode: "open", hosts: [], loopback: project.loopback };
|
|
142
|
+
const hosts = new Set([
|
|
143
|
+
...(PROVIDER_HOSTS[provider] ?? []),
|
|
144
|
+
...ecosystems.flatMap(one => REGISTRY_HOSTS[one]),
|
|
145
|
+
...project.hosts,
|
|
146
|
+
]);
|
|
147
|
+
return { version: NETWORK_POLICY_VERSION, mode: "allowlist", hosts: [...hosts].sort(), loopback: project.loopback };
|
|
148
|
+
}
|
|
149
|
+
/** The network of a launch nothing sealed one for (a planner, a check): its provider's own hosts and nothing else. */
|
|
150
|
+
export function providerOnlyNetwork(provider) {
|
|
151
|
+
return { version: NETWORK_POLICY_VERSION, mode: "allowlist", hosts: [...new Set(PROVIDER_HOSTS[provider] ?? [])].sort(), loopback: "proxy-only" };
|
|
152
|
+
}
|
|
153
|
+
/**
|
|
154
|
+
* The network an approval sealed before network policies existed runs under: a deliberate narrowing (provider hosts
|
|
155
|
+
* and the repository's registries, nothing the project added since, local services of this Mac out of reach). Never
|
|
156
|
+
* widened by a setting changed after it was approved.
|
|
157
|
+
*/
|
|
158
|
+
export function legacyNetwork(provider, ecosystems) {
|
|
159
|
+
return { ...resolveNetwork(provider, { mode: "allowlist", hosts: [], loopback: "proxy-only" }, ecosystems) };
|
|
160
|
+
}
|
|
161
|
+
/** Strict re-hydration (scope.ts profileFromJson): exactly the keys this code writes, every host normalized, or null. */
|
|
162
|
+
export function networkFromValue(value) {
|
|
163
|
+
if (value === null || typeof value !== "object" || Array.isArray(value))
|
|
164
|
+
return null;
|
|
165
|
+
const v = value;
|
|
166
|
+
const keys = Object.keys(v);
|
|
167
|
+
if (keys.length !== 4 || !["version", "mode", "hosts", "loopback"].every(key => keys.includes(key)))
|
|
168
|
+
return null;
|
|
169
|
+
if (v["version"] !== NETWORK_POLICY_VERSION)
|
|
170
|
+
return null;
|
|
171
|
+
if (v["mode"] !== "allowlist" && v["mode"] !== "open")
|
|
172
|
+
return null;
|
|
173
|
+
if (v["loopback"] !== "any" && v["loopback"] !== "proxy-only")
|
|
174
|
+
return null;
|
|
175
|
+
if (!Array.isArray(v["hosts"]) || v["hosts"].length > MAX_SNAPSHOT_HOSTS)
|
|
176
|
+
return null;
|
|
177
|
+
const hosts = [];
|
|
178
|
+
for (const one of v["hosts"]) {
|
|
179
|
+
if (typeof one !== "string" || normalizeHost(one) !== one)
|
|
180
|
+
return null;
|
|
181
|
+
hosts.push(one);
|
|
182
|
+
}
|
|
183
|
+
if (v["mode"] === "open" && hosts.length > 0)
|
|
184
|
+
return null;
|
|
185
|
+
if (new Set(hosts).size !== hosts.length || hosts.some((one, index) => index > 0 && hosts[index - 1] >= one))
|
|
186
|
+
return null;
|
|
187
|
+
return { version: NETWORK_POLICY_VERSION, mode: v["mode"], hosts, loopback: v["loopback"] };
|
|
188
|
+
}
|
|
189
|
+
/** A project setting as saved (store.ts project_network), or null when it isn't one this code wrote. */
|
|
190
|
+
export function projectNetworkFromValue(value) {
|
|
191
|
+
if (value === null || typeof value !== "object" || Array.isArray(value))
|
|
192
|
+
return null;
|
|
193
|
+
const v = value;
|
|
194
|
+
if (Object.keys(v).length !== 3 || (v["mode"] !== "allowlist" && v["mode"] !== "open") || (v["loopback"] !== "any" && v["loopback"] !== "proxy-only"))
|
|
195
|
+
return null;
|
|
196
|
+
if (!Array.isArray(v["hosts"]) || v["hosts"].length > MAX_NETWORK_HOSTS)
|
|
197
|
+
return null;
|
|
198
|
+
const hosts = v["hosts"].map(one => typeof one === "string" ? normalizeHost(one) : null);
|
|
199
|
+
if (hosts.some(one => one === null))
|
|
200
|
+
return null;
|
|
201
|
+
return { mode: v["mode"], hosts: [...new Set(hosts)].sort(), loopback: v["loopback"] };
|
|
202
|
+
}
|
|
203
|
+
/** The network in plain words, for an approval card and a run's record. */
|
|
204
|
+
export function networkWords(network) {
|
|
205
|
+
if (network === null)
|
|
206
|
+
return "Network: approved before network rules (provider and package registries only)";
|
|
207
|
+
if (network.mode === "open")
|
|
208
|
+
return `Network: open — any host${network.loopback === "proxy-only" ? "" : ", and every local service on this Mac"}`;
|
|
209
|
+
const registries = Object.entries(REGISTRY_HOSTS)
|
|
210
|
+
.filter(([, hosts]) => hosts.every(one => network.hosts.includes(one))).map(([name]) => REGISTRY_NAMES[name]);
|
|
211
|
+
const known = new Set([...Object.values(REGISTRY_HOSTS).flat(), ...Object.values(PROVIDER_HOSTS).flat()]);
|
|
212
|
+
const added = network.hosts.filter(one => !known.has(one));
|
|
213
|
+
return `Network: the agent's provider${registries.length === 0 ? "" : `, ${registries.join(", ")}`}${added.length === 0 ? "" : `, and ${added.join(", ")}`}`
|
|
214
|
+
+ (network.loopback === "any" ? "; and every local service on this Mac" : "");
|
|
215
|
+
}
|
|
216
|
+
const REGISTRY_NAMES = { npm: "npm", python: "PyPI", rust: "crates.io", go: "the Go proxy", ruby: "RubyGems", jvm: "Maven and Gradle" };
|
|
217
|
+
const HOST_LIMIT = 50;
|
|
218
|
+
/** A stored record read back strictly: anything this code didn't write reads as none. */
|
|
219
|
+
export function runNetworkFromValue(value) {
|
|
220
|
+
if (value === null || typeof value !== "object" || Array.isArray(value))
|
|
221
|
+
return null;
|
|
222
|
+
const v = value;
|
|
223
|
+
if ((v["mode"] !== "open" && v["mode"] !== "allowlist") || v["phase"] !== "agent")
|
|
224
|
+
return null;
|
|
225
|
+
const count = (n) => typeof n === "number" && Number.isSafeInteger(n) && n >= 0;
|
|
226
|
+
const out = { mode: v["mode"], phase: "agent" };
|
|
227
|
+
if (v["via"] === true)
|
|
228
|
+
out.via = true;
|
|
229
|
+
if (v["hosts"] !== undefined) {
|
|
230
|
+
if (!count(v["hosts"]))
|
|
231
|
+
return null;
|
|
232
|
+
out.hosts = v["hosts"];
|
|
233
|
+
}
|
|
234
|
+
if (v["loopback"] !== undefined) {
|
|
235
|
+
if (v["loopback"] !== "any" && v["loopback"] !== "proxy-only")
|
|
236
|
+
return null;
|
|
237
|
+
out.loopback = v["loopback"];
|
|
238
|
+
}
|
|
239
|
+
if (v["counts"] !== undefined) {
|
|
240
|
+
const c = v["counts"];
|
|
241
|
+
if (c === null || typeof c !== "object" || !count(c["connections"]) || !count(c["refused"]) || !count(c["otherHosts"]) || !Array.isArray(c["hosts"]) || c["hosts"].length > HOST_LIMIT)
|
|
242
|
+
return null;
|
|
243
|
+
const hosts = [];
|
|
244
|
+
for (const one of c["hosts"]) {
|
|
245
|
+
const h = one;
|
|
246
|
+
if (h === null || typeof h !== "object" || typeof h["host"] !== "string" || h["host"].length > 300 || !count(h["connections"]) || !count(h["refused"]))
|
|
247
|
+
return null;
|
|
248
|
+
hosts.push({ host: h["host"], connections: h["connections"], refused: h["refused"] });
|
|
249
|
+
}
|
|
250
|
+
out.counts = { connections: c["connections"], refused: c["refused"], hosts, otherHosts: c["otherHosts"] };
|
|
251
|
+
}
|
|
252
|
+
return out;
|
|
253
|
+
}
|
|
254
|
+
/** Every proxy setting a program might read, both cases, so none falls back to a direct connection the OS refuses. */
|
|
255
|
+
export function proxyEnv(proxy, loopback) {
|
|
256
|
+
const local = loopback === "proxy-only" ? "" : "localhost,127.0.0.1,::1";
|
|
257
|
+
return {
|
|
258
|
+
HTTPS_PROXY: proxy.url, https_proxy: proxy.url, HTTP_PROXY: proxy.url, http_proxy: proxy.url, ALL_PROXY: proxy.url, all_proxy: proxy.url,
|
|
259
|
+
NO_PROXY: local, no_proxy: local,
|
|
260
|
+
// Node's own fetch reads the proxy variables only when told to (Node 24 and later; earlier versions ignore it).
|
|
261
|
+
NODE_USE_ENV_PROXY: "1",
|
|
262
|
+
};
|
|
263
|
+
}
|
|
264
|
+
/**
|
|
265
|
+
* Start one run's proxy on loopback. Only a client presenting this run's credentials is served (another run's agent
|
|
266
|
+
* on the same machine can't borrow it). CONNECT goes only to port 443 and plain HTTP only to port 80, only to a host
|
|
267
|
+
* the run's list admits (matched by name before it is resolved), only to public addresses (the proxy resolves the
|
|
268
|
+
* name itself and connects to exactly the addresses it checked). A plain HTTP request's Host must be the URL's host.
|
|
269
|
+
*/
|
|
270
|
+
export async function startEgressProxy(options) {
|
|
271
|
+
const addressAllowed = options.addressAllowed ?? isPublicAddress;
|
|
272
|
+
const upstreamPort = options.upstreamPort ?? ((port) => port);
|
|
273
|
+
const lookup = options.lookup ?? (async (host) => (await dnsLookup(host, { all: true, verbatim: true })).map(one => one.address));
|
|
274
|
+
const secret = randomBytes(24).toString("base64url");
|
|
275
|
+
const expected = Buffer.from(`Basic ${Buffer.from(`run:${secret}`).toString("base64")}`);
|
|
276
|
+
const sockets = new Set();
|
|
277
|
+
const byHost = new Map();
|
|
278
|
+
let connections = 0, refused = 0, otherHosts = 0;
|
|
279
|
+
const count = (host, allowed, why) => {
|
|
280
|
+
if (allowed)
|
|
281
|
+
connections++;
|
|
282
|
+
else
|
|
283
|
+
refused++;
|
|
284
|
+
const key = normalizeHost(host) ?? "(not a host name)";
|
|
285
|
+
let entry = byHost.get(key);
|
|
286
|
+
if (entry === undefined && byHost.size >= HOST_LIMIT) {
|
|
287
|
+
otherHosts++;
|
|
288
|
+
return;
|
|
289
|
+
}
|
|
290
|
+
if (entry === undefined) {
|
|
291
|
+
entry = { connections: 0, refused: 0 };
|
|
292
|
+
byHost.set(key, entry);
|
|
293
|
+
}
|
|
294
|
+
if (allowed)
|
|
295
|
+
entry.connections++;
|
|
296
|
+
else
|
|
297
|
+
entry.refused++;
|
|
298
|
+
try {
|
|
299
|
+
options.decided?.(key, allowed, why);
|
|
300
|
+
}
|
|
301
|
+
catch { /* a listener's trouble is not the build's */ }
|
|
302
|
+
};
|
|
303
|
+
const authorized = (incoming) => {
|
|
304
|
+
const given = Buffer.from(String(incoming.headers["proxy-authorization"] ?? ""));
|
|
305
|
+
return given.length === expected.length && timingSafeEqual(given, expected);
|
|
306
|
+
};
|
|
307
|
+
/** The addresses a request may reach, or why it may not. */
|
|
308
|
+
const admit = async (host, port, wantPort) => {
|
|
309
|
+
if (port !== wantPort)
|
|
310
|
+
return { ok: false, why: `port ${port} is not allowed` };
|
|
311
|
+
if (!hostAllowed(host, options.hosts))
|
|
312
|
+
return { ok: false, why: "not an approved host" };
|
|
313
|
+
let addresses;
|
|
314
|
+
try {
|
|
315
|
+
addresses = await lookup(normalizeHost(host));
|
|
316
|
+
}
|
|
317
|
+
catch {
|
|
318
|
+
return { ok: false, why: "the host does not resolve" };
|
|
319
|
+
}
|
|
320
|
+
if (addresses.length === 0)
|
|
321
|
+
return { ok: false, why: "the host does not resolve" };
|
|
322
|
+
if (!addresses.every(addressAllowed))
|
|
323
|
+
return { ok: false, why: "not a public internet address" };
|
|
324
|
+
return { ok: true, addresses };
|
|
325
|
+
};
|
|
326
|
+
const server = createServer((incoming, outgoing) => {
|
|
327
|
+
void (async () => {
|
|
328
|
+
if (!authorized(incoming)) {
|
|
329
|
+
outgoing.writeHead(407, { "proxy-authenticate": 'Basic realm="toolroll"' }).end();
|
|
330
|
+
return;
|
|
331
|
+
}
|
|
332
|
+
let url;
|
|
333
|
+
try {
|
|
334
|
+
url = new URL(incoming.url ?? "");
|
|
335
|
+
}
|
|
336
|
+
catch {
|
|
337
|
+
outgoing.writeHead(400).end();
|
|
338
|
+
return;
|
|
339
|
+
}
|
|
340
|
+
const hostHeader = String(incoming.headers.host ?? "").toLowerCase();
|
|
341
|
+
const sameHost = hostHeader === url.host.toLowerCase() || hostHeader === url.hostname.toLowerCase() || (url.port === "" && hostHeader === `${url.hostname.toLowerCase()}:80`);
|
|
342
|
+
if (url.protocol !== "http:" || url.username !== "" || url.password !== "" || !sameHost) {
|
|
343
|
+
count(url.hostname, false, "not a plain web request for its own host");
|
|
344
|
+
outgoing.writeHead(403, { "content-type": "text/plain" }).end("Toolroll: this build may reach only its approved hosts.");
|
|
345
|
+
return;
|
|
346
|
+
}
|
|
347
|
+
const reached = await admit(url.hostname, Number(url.port || 80), 80);
|
|
348
|
+
if (!reached.ok) {
|
|
349
|
+
count(url.hostname, false, reached.why);
|
|
350
|
+
outgoing.writeHead(403, { "content-type": "text/plain" }).end(`Toolroll: ${url.hostname} is not one of this build's approved hosts.`);
|
|
351
|
+
return;
|
|
352
|
+
}
|
|
353
|
+
const upstream = await connectFirst(reached.addresses, upstreamPort(80));
|
|
354
|
+
if (upstream === null) {
|
|
355
|
+
outgoing.writeHead(502).end();
|
|
356
|
+
return;
|
|
357
|
+
}
|
|
358
|
+
count(url.hostname, true, "approved");
|
|
359
|
+
sockets.add(upstream);
|
|
360
|
+
upstream.on("close", () => sockets.delete(upstream));
|
|
361
|
+
const headers = { ...incoming.headers };
|
|
362
|
+
delete headers["proxy-connection"];
|
|
363
|
+
delete headers["proxy-authorization"];
|
|
364
|
+
const forward = httpRequest({ createConnection: () => upstream, method: incoming.method, path: `${url.pathname}${url.search}`, headers, setHost: false }, answer => {
|
|
365
|
+
outgoing.writeHead(answer.statusCode ?? 502, answer.headers);
|
|
366
|
+
answer.pipe(outgoing);
|
|
367
|
+
});
|
|
368
|
+
forward.on("error", () => { if (!outgoing.headersSent)
|
|
369
|
+
outgoing.writeHead(502); outgoing.end(); });
|
|
370
|
+
incoming.pipe(forward);
|
|
371
|
+
})();
|
|
372
|
+
});
|
|
373
|
+
server.on("connect", (incoming, client, head) => {
|
|
374
|
+
sockets.add(client);
|
|
375
|
+
client.on("close", () => sockets.delete(client));
|
|
376
|
+
client.on("error", () => client.destroy());
|
|
377
|
+
void (async () => {
|
|
378
|
+
if (!authorized(incoming)) {
|
|
379
|
+
client.end('HTTP/1.1 407 Proxy Authentication Required\r\nProxy-Authenticate: Basic realm="toolroll"\r\nContent-Length: 0\r\n\r\n');
|
|
380
|
+
return;
|
|
381
|
+
}
|
|
382
|
+
const match = /^([^:[\]\s]+):(\d{1,5})$/.exec(incoming.url ?? "");
|
|
383
|
+
const reached = match === null ? { ok: false, why: "not a host name and port" } : await admit(match[1], Number(match[2]), 443);
|
|
384
|
+
if (!reached.ok) {
|
|
385
|
+
count(match?.[1] ?? String(incoming.url ?? ""), false, reached.why);
|
|
386
|
+
client.end("HTTP/1.1 403 Forbidden\r\nContent-Length: 0\r\n\r\n");
|
|
387
|
+
return;
|
|
388
|
+
}
|
|
389
|
+
const upstream = await connectFirst(reached.addresses, upstreamPort(443));
|
|
390
|
+
if (upstream === null) {
|
|
391
|
+
if (!client.destroyed)
|
|
392
|
+
client.end("HTTP/1.1 502 Bad Gateway\r\nContent-Length: 0\r\n\r\n");
|
|
393
|
+
return;
|
|
394
|
+
}
|
|
395
|
+
count(match[1], true, "approved");
|
|
396
|
+
sockets.add(upstream);
|
|
397
|
+
upstream.on("close", () => { sockets.delete(upstream); client.destroy(); });
|
|
398
|
+
upstream.on("error", () => client.destroy());
|
|
399
|
+
client.write("HTTP/1.1 200 Connection Established\r\n\r\n");
|
|
400
|
+
if (head.length > 0)
|
|
401
|
+
upstream.write(head);
|
|
402
|
+
upstream.pipe(client);
|
|
403
|
+
client.pipe(upstream);
|
|
404
|
+
})();
|
|
405
|
+
});
|
|
406
|
+
server.on("connection", socket => { sockets.add(socket); socket.on("close", () => sockets.delete(socket)); });
|
|
407
|
+
await new Promise((done, fail) => {
|
|
408
|
+
server.once("error", fail);
|
|
409
|
+
server.listen(0, "127.0.0.1", () => { server.off("error", fail); done(); });
|
|
410
|
+
});
|
|
411
|
+
const address = server.address();
|
|
412
|
+
const port = typeof address === "object" && address !== null ? address.port : 0;
|
|
413
|
+
return {
|
|
414
|
+
url: `http://run:${secret}@127.0.0.1:${port}`,
|
|
415
|
+
port,
|
|
416
|
+
counts: () => ({ connections, refused, hosts: [...byHost].map(([host, one]) => ({ host, ...one })).sort((a, b) => b.connections + b.refused - (a.connections + a.refused) || a.host.localeCompare(b.host)), otherHosts }),
|
|
417
|
+
close: () => new Promise(done => {
|
|
418
|
+
for (const socket of sockets)
|
|
419
|
+
socket.destroy();
|
|
420
|
+
server.close(() => done());
|
|
421
|
+
}),
|
|
422
|
+
};
|
|
423
|
+
}
|
package/dist/evidence-pack.d.ts
CHANGED
package/dist/evidence-pack.js
CHANGED
|
@@ -55,7 +55,7 @@ export function evidencePack(store, taskId, access, who, now, evidenceRoot, chec
|
|
|
55
55
|
goal: scope.goal, outOfScope: scope.outOfScope, touches: scope.touches, acceptance: scope.acceptance.map(one => ({ id: one.id, statement: one.statement })),
|
|
56
56
|
budgetUsd: scope.budgetMicrousd === null ? null : scope.budgetMicrousd / 1_000_000, digest: scope.digest,
|
|
57
57
|
approved: scope.approvedBy === null || scope.approvedAt === null || scope.approvedDigest === null ? null
|
|
58
|
-
: { by: scope.approvedBy, at: scope.approvedAt, digest: scope.approvedDigest, basis: scope.approvalBasis === "mode" ? "mode" : "person", current: scope.approvedDigest === scope.digest },
|
|
58
|
+
: { by: scope.approvedBy, at: scope.approvedAt, digest: scope.approvedDigest, basis: scope.approvalBasis === "mode" ? "mode" : scope.approvalBasis === "grant" ? "flow" : "person", current: scope.approvedDigest === scope.digest },
|
|
59
59
|
writtenBy: history.authors,
|
|
60
60
|
approvals: history.votes,
|
|
61
61
|
},
|
|
@@ -206,7 +206,7 @@ export function evidencePackHtml(pack) {
|
|
|
206
206
|
...(scope.touches.length ? [["Touches", joinHtml(scope.touches, ", ")]] : []),
|
|
207
207
|
...(scope.acceptance.length ? [["Acceptance", html `<ol class="criteria">${scope.acceptance.map(one => html `<li>${one.statement}</li>`)}</ol>`]] : []),
|
|
208
208
|
...(scope.writtenBy.length ? [["Scope written by", joinHtml([...new Set(scope.writtenBy.map(one => one.author))], ", ")]] : []),
|
|
209
|
-
["Approved", approved === null ? html `Not approved` : html `${approved.by}${approved.basis === "mode" ? " (operating mode)" : ""} · ${when(approved.at)}${approved.current ? "" : " · the scope changed after this"}`],
|
|
209
|
+
["Approved", approved === null ? html `Not approved` : html `${approved.by}${approved.basis === "mode" ? " (operating mode)" : approved.basis === "flow" ? " (flow runs on its own)" : ""} · ${when(approved.at)}${approved.current ? "" : " · the scope changed after this"}`],
|
|
210
210
|
...(approvals.length > 1 ? [["Approvals", joinHtml(approvals.map(one => html `${one.approver} · ${when(one.at)}`), html `<br>`)]] : []),
|
|
211
211
|
["Scope digest", short(scope.digest)],
|
|
212
212
|
]),
|