@cluesmith/codev 3.3.0 → 3.3.2
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/dashboard-dist/assets/{index-JCbkpAcV.js → index-D3XaKpZW.js} +45 -43
- package/dashboard-dist/assets/index-D3XaKpZW.js.map +1 -0
- package/dashboard-dist/index.html +1 -1
- package/dist/agent-farm/cli.d.ts.map +1 -1
- package/dist/agent-farm/cli.js +99 -36
- package/dist/agent-farm/cli.js.map +1 -1
- package/dist/agent-farm/commands/reset/constants.d.ts +119 -1
- package/dist/agent-farm/commands/reset/constants.d.ts.map +1 -1
- package/dist/agent-farm/commands/reset/constants.js +119 -1
- package/dist/agent-farm/commands/reset/constants.js.map +1 -1
- package/dist/agent-farm/commands/reset/context.d.ts +20 -2
- package/dist/agent-farm/commands/reset/context.d.ts.map +1 -1
- package/dist/agent-farm/commands/reset/context.js +44 -3
- package/dist/agent-farm/commands/reset/context.js.map +1 -1
- package/dist/agent-farm/commands/reset/index.d.ts +7 -7
- package/dist/agent-farm/commands/reset/index.js +8 -8
- package/dist/agent-farm/commands/reset/index.js.map +1 -1
- package/dist/agent-farm/commands/reset/receipt.d.ts +1 -1
- package/dist/agent-farm/commands/reset/receipt.d.ts.map +1 -1
- package/dist/agent-farm/commands/reset/receipt.js +19 -8
- package/dist/agent-farm/commands/reset/receipt.js.map +1 -1
- package/dist/agent-farm/commands/reset/reorient.d.ts +1 -1
- package/dist/agent-farm/commands/reset/reorient.d.ts.map +1 -1
- package/dist/agent-farm/commands/reset/reorient.js +12 -5
- package/dist/agent-farm/commands/reset/reorient.js.map +1 -1
- package/dist/agent-farm/commands/reset/self.d.ts +335 -0
- package/dist/agent-farm/commands/reset/self.d.ts.map +1 -0
- package/dist/agent-farm/commands/reset/self.js +636 -0
- package/dist/agent-farm/commands/reset/self.js.map +1 -0
- package/dist/agent-farm/commands/reset.d.ts +18 -3
- package/dist/agent-farm/commands/reset.d.ts.map +1 -1
- package/dist/agent-farm/commands/reset.js +55 -33
- package/dist/agent-farm/commands/reset.js.map +1 -1
- package/dist/agent-farm/commands/self-refresh.d.ts +60 -0
- package/dist/agent-farm/commands/self-refresh.d.ts.map +1 -0
- package/dist/agent-farm/commands/self-refresh.js +506 -0
- package/dist/agent-farm/commands/self-refresh.js.map +1 -0
- package/dist/agent-farm/commands/send.d.ts.map +1 -1
- package/dist/agent-farm/commands/send.js +27 -2
- package/dist/agent-farm/commands/send.js.map +1 -1
- package/dist/agent-farm/commands/spawn-worktree.js +1 -1
- package/dist/agent-farm/commands/spawn-worktree.js.map +1 -1
- package/dist/agent-farm/commands/spawn.d.ts +7 -0
- package/dist/agent-farm/commands/spawn.d.ts.map +1 -1
- package/dist/agent-farm/commands/spawn.js +13 -1
- package/dist/agent-farm/commands/spawn.js.map +1 -1
- package/dist/agent-farm/commands/tower.d.ts.map +1 -1
- package/dist/agent-farm/commands/tower.js +13 -0
- package/dist/agent-farm/commands/tower.js.map +1 -1
- package/dist/agent-farm/lib/reconnect-backoff.d.ts +10 -0
- package/dist/agent-farm/lib/reconnect-backoff.d.ts.map +1 -1
- package/dist/agent-farm/lib/reconnect-backoff.js +10 -0
- package/dist/agent-farm/lib/reconnect-backoff.js.map +1 -1
- package/dist/agent-farm/lib/tower-client.d.ts +12 -0
- package/dist/agent-farm/lib/tower-client.d.ts.map +1 -1
- package/dist/agent-farm/lib/tower-client.js +12 -0
- package/dist/agent-farm/lib/tower-client.js.map +1 -1
- package/dist/agent-farm/lib/tunnel-client.d.ts.map +1 -1
- package/dist/agent-farm/lib/tunnel-client.js +7 -3
- package/dist/agent-farm/lib/tunnel-client.js.map +1 -1
- package/dist/agent-farm/servers/canvas-relay.d.ts +60 -0
- package/dist/agent-farm/servers/canvas-relay.d.ts.map +1 -0
- package/dist/agent-farm/servers/canvas-relay.js +376 -0
- package/dist/agent-farm/servers/canvas-relay.js.map +1 -0
- package/dist/agent-farm/servers/gate-profiles.d.ts +1 -1
- package/dist/agent-farm/servers/gate-profiles.js +1 -1
- package/dist/agent-farm/servers/mailbox-delivery.d.ts +141 -13
- package/dist/agent-farm/servers/mailbox-delivery.d.ts.map +1 -1
- package/dist/agent-farm/servers/mailbox-delivery.js +172 -16
- package/dist/agent-farm/servers/mailbox-delivery.js.map +1 -1
- package/dist/agent-farm/servers/mailbox-wiring.d.ts +24 -2
- package/dist/agent-farm/servers/mailbox-wiring.d.ts.map +1 -1
- package/dist/agent-farm/servers/mailbox-wiring.js +92 -32
- package/dist/agent-farm/servers/mailbox-wiring.js.map +1 -1
- package/dist/agent-farm/servers/message-write.d.ts +58 -18
- package/dist/agent-farm/servers/message-write.d.ts.map +1 -1
- package/dist/agent-farm/servers/message-write.js +73 -28
- package/dist/agent-farm/servers/message-write.js.map +1 -1
- package/dist/agent-farm/servers/overview.d.ts +19 -0
- package/dist/agent-farm/servers/overview.d.ts.map +1 -1
- package/dist/agent-farm/servers/overview.js +78 -1
- package/dist/agent-farm/servers/overview.js.map +1 -1
- package/dist/agent-farm/servers/render-gate.d.ts +13 -0
- package/dist/agent-farm/servers/render-gate.d.ts.map +1 -1
- package/dist/agent-farm/servers/render-gate.js +21 -0
- package/dist/agent-farm/servers/render-gate.js.map +1 -1
- package/dist/agent-farm/servers/session-submit.d.ts +221 -28
- package/dist/agent-farm/servers/session-submit.d.ts.map +1 -1
- package/dist/agent-farm/servers/session-submit.js +334 -43
- package/dist/agent-farm/servers/session-submit.js.map +1 -1
- package/dist/agent-farm/servers/tower-routes.d.ts.map +1 -1
- package/dist/agent-farm/servers/tower-routes.js +269 -64
- package/dist/agent-farm/servers/tower-routes.js.map +1 -1
- package/dist/agent-farm/servers/tower-server.js +25 -3
- package/dist/agent-farm/servers/tower-server.js.map +1 -1
- package/dist/agent-farm/servers/tower-terminals.d.ts +6 -0
- package/dist/agent-farm/servers/tower-terminals.d.ts.map +1 -1
- package/dist/agent-farm/servers/tower-terminals.js +30 -5
- package/dist/agent-farm/servers/tower-terminals.js.map +1 -1
- package/dist/agent-farm/servers/tower-tunnel.d.ts.map +1 -1
- package/dist/agent-farm/servers/tower-tunnel.js +14 -0
- package/dist/agent-farm/servers/tower-tunnel.js.map +1 -1
- package/dist/agent-farm/servers/tower-websocket.d.ts +6 -1
- package/dist/agent-farm/servers/tower-websocket.d.ts.map +1 -1
- package/dist/agent-farm/servers/tower-websocket.js +90 -37
- package/dist/agent-farm/servers/tower-websocket.js.map +1 -1
- package/dist/agent-farm/types.d.ts +37 -2
- package/dist/agent-farm/types.d.ts.map +1 -1
- package/dist/agent-farm/utils/harness.d.ts +3 -3
- package/dist/agent-farm/utils/harness.js +1 -1
- package/dist/agent-farm/utils/message-format.d.ts +67 -3
- package/dist/agent-farm/utils/message-format.d.ts.map +1 -1
- package/dist/agent-farm/utils/message-format.js +127 -14
- package/dist/agent-farm/utils/message-format.js.map +1 -1
- package/dist/agent-farm/utils/server-utils.d.ts +86 -4
- package/dist/agent-farm/utils/server-utils.d.ts.map +1 -1
- package/dist/agent-farm/utils/server-utils.js +303 -5
- package/dist/agent-farm/utils/server-utils.js.map +1 -1
- package/dist/commands/porch/context-refresh.d.ts +181 -0
- package/dist/commands/porch/context-refresh.d.ts.map +1 -0
- package/dist/commands/porch/context-refresh.js +270 -0
- package/dist/commands/porch/context-refresh.js.map +1 -0
- package/dist/commands/porch/index.d.ts +6 -0
- package/dist/commands/porch/index.d.ts.map +1 -1
- package/dist/commands/porch/index.js +46 -0
- package/dist/commands/porch/index.js.map +1 -1
- package/dist/commands/porch/next.d.ts +0 -9
- package/dist/commands/porch/next.d.ts.map +1 -1
- package/dist/commands/porch/next.js +133 -4
- package/dist/commands/porch/next.js.map +1 -1
- package/dist/commands/porch/protocol.d.ts.map +1 -1
- package/dist/commands/porch/protocol.js +102 -0
- package/dist/commands/porch/protocol.js.map +1 -1
- package/dist/commands/porch/types.d.ts +61 -0
- package/dist/commands/porch/types.d.ts.map +1 -1
- package/dist/lib/forge-contracts.d.ts +9 -0
- package/dist/lib/forge-contracts.d.ts.map +1 -1
- package/dist/lib/forge-contracts.js.map +1 -1
- package/dist/lib/github.d.ts +15 -1
- package/dist/lib/github.d.ts.map +1 -1
- package/dist/lib/github.js +17 -0
- package/dist/lib/github.js.map +1 -1
- package/dist/lib/self-refresh-invocation.d.ts +60 -0
- package/dist/lib/self-refresh-invocation.d.ts.map +1 -0
- package/dist/lib/self-refresh-invocation.js +64 -0
- package/dist/lib/self-refresh-invocation.js.map +1 -0
- package/dist/lib/test-env.d.ts +50 -0
- package/dist/lib/test-env.d.ts.map +1 -1
- package/dist/lib/test-env.js +76 -0
- package/dist/lib/test-env.js.map +1 -1
- package/dist/terminal/attach-replay.d.ts +39 -0
- package/dist/terminal/attach-replay.d.ts.map +1 -0
- package/dist/terminal/attach-replay.js +76 -0
- package/dist/terminal/attach-replay.js.map +1 -0
- package/dist/terminal/pty-manager.d.ts.map +1 -1
- package/dist/terminal/pty-manager.js +35 -17
- package/dist/terminal/pty-manager.js.map +1 -1
- package/dist/terminal/pty-session.d.ts +81 -12
- package/dist/terminal/pty-session.d.ts.map +1 -1
- package/dist/terminal/pty-session.js +113 -25
- package/dist/terminal/pty-session.js.map +1 -1
- package/dist/terminal/session-screen.d.ts +25 -0
- package/dist/terminal/session-screen.d.ts.map +1 -1
- package/dist/terminal/session-screen.js +53 -8
- package/dist/terminal/session-screen.js.map +1 -1
- package/package.json +10 -7
- package/scripts/forge/gitea/pr-view.sh +4 -1
- package/scripts/forge/github/pr-view.sh +1 -1
- package/scripts/forge/gitlab/pr-view.sh +4 -1
- package/skeleton/.claude/skills/afx/SKILL.md +54 -0
- package/skeleton/.claude/skills/arch-save/SKILL.md +8 -2
- package/skeleton/.claude/skills/builder-refresh/SKILL.md +138 -0
- package/skeleton/.codex/skills/afx/SKILL.md +54 -0
- package/skeleton/.codex/skills/arch-save/SKILL.md +8 -2
- package/skeleton/.codex/skills/builder-refresh/SKILL.md +138 -0
- package/skeleton/protocol-schema.json +17 -0
- package/skeleton/protocols/air/protocol.json +1 -1
- package/skeleton/protocols/aspir/protocol.json +5 -1
- package/skeleton/protocols/bugfix/protocol.json +1 -1
- package/skeleton/protocols/experiment/protocol.json +1 -1
- package/skeleton/protocols/maintain/protocol.json +1 -1
- package/skeleton/protocols/pir/builder-prompt.md +3 -2
- package/skeleton/protocols/pir/prompts/implement.md +1 -1
- package/skeleton/protocols/pir/prompts/plan.md +1 -1
- package/skeleton/protocols/pir/prompts/review.md +1 -1
- package/skeleton/protocols/pir/protocol.json +1 -1
- package/skeleton/protocols/pir/protocol.md +4 -4
- package/skeleton/protocols/protocol-schema.json +17 -0
- package/skeleton/protocols/research/protocol.json +1 -1
- package/skeleton/protocols/spike/protocol.json +1 -1
- package/skeleton/protocols/spir/protocol.json +5 -1
- package/skeleton/resources/commands/agent-farm.md +14 -11
- package/skeleton/roles/builder.md +3 -4
- package/templates/3d-viewer.html +32 -4
- package/templates/open.html +58 -12
- package/templates/tower.html +26 -7
- package/templates/vendor/three-3MFLoader.js +1478 -0
- package/templates/vendor/three-STLLoader.js +410 -0
- package/templates/vendor/three-TrackballControls.js +828 -0
- package/templates/vendor/three-fflate.module.js +2474 -0
- package/templates/vendor/three.module.js +53044 -0
- package/dashboard-dist/assets/index-JCbkpAcV.js.map +0 -1
|
@@ -19,12 +19,94 @@ export declare function readBody(req: http.IncomingMessage, maxSize?: number): P
|
|
|
19
19
|
*/
|
|
20
20
|
export declare function parseJsonBody(req: http.IncomingMessage, maxSize?: number): Promise<Record<string, unknown>>;
|
|
21
21
|
/**
|
|
22
|
-
*
|
|
23
|
-
*
|
|
22
|
+
* The expected local key, cached after first read. Issues the key if missing
|
|
23
|
+
* (Tower is the owner). Returns null and stays fail-closed if the key cannot be
|
|
24
|
+
* read or created (e.g. an unwritable `~/.agent-farm`).
|
|
25
|
+
*/
|
|
26
|
+
export declare function getExpectedKey(): string | null;
|
|
27
|
+
/**
|
|
28
|
+
* Reset the cached key. Test-only seam; also lets a future rotation path force
|
|
29
|
+
* a re-read. Not wired to any runtime rotation in this change.
|
|
30
|
+
*/
|
|
31
|
+
export declare function resetExpectedKeyCache(): void;
|
|
32
|
+
/**
|
|
33
|
+
* Constant-time key comparison. `timingSafeEqual` throws on unequal-length
|
|
34
|
+
* buffers, so length is checked first (a length mismatch is an immediate,
|
|
35
|
+
* non-secret reject).
|
|
36
|
+
*/
|
|
37
|
+
export declare function keysMatch(presented: string, expected: string): boolean;
|
|
38
|
+
/**
|
|
39
|
+
* Routes intentionally reachable without the key. Kept deliberately narrow:
|
|
40
|
+
* pre-auth liveness/version probes, the Tower launcher shell, the React
|
|
41
|
+
* dashboard's static assets (the page loads keyless, then authenticates its
|
|
42
|
+
* own API/WebSocket calls with the key), the annotator shell + vendor files,
|
|
43
|
+
* and the cloud OAuth callback. Everything else requires the key.
|
|
44
|
+
*
|
|
45
|
+
* The entries here are the paths a *browser navigation* reaches — a top-level
|
|
46
|
+
* redirect, an iframe load, a `<script>`/`<link>` tag — none of which can
|
|
47
|
+
* attach the `codev-tower-key` header. Each one therefore carries either no
|
|
48
|
+
* secret or its own credential; adding a route here means proving that.
|
|
49
|
+
*
|
|
50
|
+
* The privileged workspace `file` reader and every `api/` or `ws/` subpath are
|
|
51
|
+
* explicitly excluded so a static-asset carve-out never exposes a data route.
|
|
52
|
+
*/
|
|
53
|
+
export declare function isPublicRoute(method: string, pathname: string): boolean;
|
|
54
|
+
/**
|
|
55
|
+
* CORS origin allowlist (advisory Layer 3). Replaces the previous
|
|
56
|
+
* reflect-any-`https://` behavior. Allowed: loopback origins on any port
|
|
57
|
+
* (`http://localhost[:port]`, `http://127.0.0.1[:port]`) plus any origins an
|
|
58
|
+
* operator lists in `CODEV_TOWER_ALLOWED_ORIGINS` (comma-separated, exact
|
|
59
|
+
* match) for a tunnel/proxy deployment. Secure by default: no wildcard, no
|
|
60
|
+
* scheme-only reflection. CORS is defense-in-depth; the key check is the
|
|
61
|
+
* actual control.
|
|
62
|
+
*/
|
|
63
|
+
export declare function isAllowedOrigin(origin: string): boolean;
|
|
64
|
+
/**
|
|
65
|
+
* Host allowlist (advisory GHSA-xvjp-7748-v88v). A DNS-rebinding guard for the
|
|
66
|
+
* key-bearing dashboard shell: the key is injected into the (public) shell, so a
|
|
67
|
+
* browser rebound to Tower's address would carry the attacker's hostname in
|
|
68
|
+
* `Host` and be rejected before the shell is served. Allowed: loopback hostnames
|
|
69
|
+
* and the hostnames of any `CODEV_TOWER_ALLOWED_ORIGINS` (the same var the CORS
|
|
70
|
+
* allowlist uses, so a tunnel/proxy deployment configures both).
|
|
71
|
+
*
|
|
72
|
+
* In BRIDGE_MODE the operator has deliberately exposed Tower on the network, so
|
|
73
|
+
* the allowlist ALSO accepts IP-literal Hosts — a LAN client reaches Tower by IP
|
|
74
|
+
* (`http://192.168.1.5:4100/`), while DNS rebinding requires a host *name*, which
|
|
75
|
+
* stays rejected even in bridge mode. Hostname-based LAN access uses the
|
|
76
|
+
* `CODEV_TOWER_ALLOWED_ORIGINS` escape hatch. The key (mandatory under bridge
|
|
77
|
+
* mode) remains the primary control and is checked separately for keyed routes;
|
|
78
|
+
* this relaxation never weakens it.
|
|
79
|
+
*/
|
|
80
|
+
export declare function isAllowedHost(hostHeader: string | undefined): boolean;
|
|
81
|
+
/**
|
|
82
|
+
* Security: decide whether an HTTP request may proceed.
|
|
83
|
+
*
|
|
84
|
+
* Public-allowlisted routes pass keyless; every other route must present a
|
|
85
|
+
* `codev-tower-key` header that constant-time-matches the expected local key.
|
|
86
|
+
* Fails closed when the expected key is unavailable.
|
|
87
|
+
*
|
|
24
88
|
* @param req - HTTP incoming message
|
|
25
|
-
* @returns true
|
|
89
|
+
* @returns true if the request is authorized
|
|
90
|
+
*/
|
|
91
|
+
export declare function isRequestAllowed(req: http.IncomingMessage): boolean;
|
|
92
|
+
/**
|
|
93
|
+
* The subprotocol the server echoes back on a terminal WebSocket handshake:
|
|
94
|
+
* the non-secret marker if the client offered it, else none. NEVER echoes the
|
|
95
|
+
* `codev-key.<key>` token — echoing it would leak the key into a response
|
|
96
|
+
* header. Used as the `ws` server's `handleProtocols` (advisory
|
|
97
|
+
* GHSA-xvjp-7748-v88v). Exported so the echo contract is unit-testable.
|
|
98
|
+
*/
|
|
99
|
+
export declare function selectWsSubprotocol(offered: Set<string>): string | false;
|
|
100
|
+
/**
|
|
101
|
+
* Security: decide whether a WebSocket upgrade may proceed. Validated at the
|
|
102
|
+
* handshake (before any PTY attach), independent of the `Origin` header so a
|
|
103
|
+
* missing Origin can never degrade into an auth bypass. Fails closed when the
|
|
104
|
+
* expected key is unavailable.
|
|
105
|
+
*
|
|
106
|
+
* @param req - HTTP upgrade request
|
|
107
|
+
* @returns true if the upgrade is authorized
|
|
26
108
|
*/
|
|
27
|
-
export declare function
|
|
109
|
+
export declare function isWebSocketAllowed(req: http.IncomingMessage): boolean;
|
|
28
110
|
/**
|
|
29
111
|
* Validate a bind host value for server.listen().
|
|
30
112
|
*
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"server-utils.d.ts","sourceRoot":"","sources":["../../../src/agent-farm/utils/server-utils.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,OAAO,KAAK,KAAK,IAAI,MAAM,WAAW,CAAC;
|
|
1
|
+
{"version":3,"file":"server-utils.d.ts","sourceRoot":"","sources":["../../../src/agent-farm/utils/server-utils.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,OAAO,KAAK,KAAK,IAAI,MAAM,WAAW,CAAC;AAKvC;;GAEG;AACH,wBAAgB,UAAU,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,CAO9C;AAED;;GAEG;AACH,wBAAgB,QAAQ,CAAC,GAAG,EAAE,IAAI,CAAC,eAAe,EAAE,OAAO,SAAc,GAAG,OAAO,CAAC,MAAM,CAAC,CAgB1F;AAED;;;;GAIG;AACH,wBAAgB,aAAa,CAAC,GAAG,EAAE,IAAI,CAAC,eAAe,EAAE,OAAO,SAAc,GAAG,OAAO,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,CAyBhH;AAsBD;;;;GAIG;AACH,wBAAgB,cAAc,IAAI,MAAM,GAAG,IAAI,CAS9C;AAED;;;GAGG;AACH,wBAAgB,qBAAqB,IAAI,IAAI,CAE5C;AAED;;;;GAIG;AACH,wBAAgB,SAAS,CAAC,SAAS,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,OAAO,CAKtE;AAED;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,aAAa,CAAC,MAAM,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,OAAO,CA2CvE;AAED;;;;;;;;GAQG;AACH,wBAAgB,eAAe,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAWvD;AAwCD;;;;;;;;;;;;;;;GAeG;AACH,wBAAgB,aAAa,CAAC,UAAU,EAAE,MAAM,GAAG,SAAS,GAAG,OAAO,CASrE;AAmBD;;;;;;;;;GASG;AACH,wBAAgB,gBAAgB,CAAC,GAAG,EAAE,IAAI,CAAC,eAAe,GAAG,OAAO,CAsBnE;AAqBD;;;;;;GAMG;AACH,wBAAgB,mBAAmB,CAAC,OAAO,EAAE,GAAG,CAAC,MAAM,CAAC,GAAG,MAAM,GAAG,KAAK,CAExE;AAED;;;;;;;;GAQG;AACH,wBAAgB,kBAAkB,CAAC,GAAG,EAAE,IAAI,CAAC,eAAe,GAAG,OAAO,CAUrE;AACD;;;;;;;;;;;GAWG;AACH,wBAAgB,YAAY,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAiCjD"}
|
|
@@ -3,6 +3,9 @@
|
|
|
3
3
|
* Extracted from tower-server.ts and open-server.ts
|
|
4
4
|
* to eliminate code duplication (Maintenance Run 0004)
|
|
5
5
|
*/
|
|
6
|
+
import { timingSafeEqual } from 'node:crypto';
|
|
7
|
+
import { ensureLocalKey } from '@cluesmith/codev-core/auth';
|
|
8
|
+
import { TOWER_KEY_HEADER, LEGACY_WEB_KEY_HEADER, WS_MARKER_PROTOCOL, WS_KEY_PROTOCOL_PREFIX } from '@cluesmith/codev-types';
|
|
6
9
|
/**
|
|
7
10
|
* HTML-escape a string to prevent XSS
|
|
8
11
|
*/
|
|
@@ -63,14 +66,309 @@ export function parseJsonBody(req, maxSize = 1024 * 1024) {
|
|
|
63
66
|
req.on('error', reject);
|
|
64
67
|
});
|
|
65
68
|
}
|
|
69
|
+
// ============================================================================
|
|
70
|
+
// Request authentication (advisory GHSA-xvjp-7748-v88v)
|
|
71
|
+
// ============================================================================
|
|
72
|
+
//
|
|
73
|
+
// Tower's local HTTP + WebSocket API reaches privileged local operations, so
|
|
74
|
+
// every request that is not on the narrow public-route allowlist must present
|
|
75
|
+
// the shared local key (`~/.agent-farm/local-key`) in the `codev-tower-key`
|
|
76
|
+
// header. Enforcement is server-side only (server/client isolation, #1189):
|
|
77
|
+
// clients merely transport the key.
|
|
78
|
+
// Wire-contract names (header + WS subprotocols) live in `@cluesmith/codev-types`
|
|
79
|
+
// so the server and every client share one source of truth.
|
|
66
80
|
/**
|
|
67
|
-
*
|
|
68
|
-
*
|
|
81
|
+
* Cached expected key. `undefined` = not yet loaded; `null` = load failed
|
|
82
|
+
* (fail closed — reject every authenticated request). Tower owns generation,
|
|
83
|
+
* so under normal operation the key file exists after boot.
|
|
84
|
+
*/
|
|
85
|
+
let cachedExpectedKey;
|
|
86
|
+
/**
|
|
87
|
+
* The expected local key, cached after first read. Issues the key if missing
|
|
88
|
+
* (Tower is the owner). Returns null and stays fail-closed if the key cannot be
|
|
89
|
+
* read or created (e.g. an unwritable `~/.agent-farm`).
|
|
90
|
+
*/
|
|
91
|
+
export function getExpectedKey() {
|
|
92
|
+
if (cachedExpectedKey === undefined) {
|
|
93
|
+
try {
|
|
94
|
+
cachedExpectedKey = ensureLocalKey() || null;
|
|
95
|
+
}
|
|
96
|
+
catch {
|
|
97
|
+
cachedExpectedKey = null;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
return cachedExpectedKey;
|
|
101
|
+
}
|
|
102
|
+
/**
|
|
103
|
+
* Reset the cached key. Test-only seam; also lets a future rotation path force
|
|
104
|
+
* a re-read. Not wired to any runtime rotation in this change.
|
|
105
|
+
*/
|
|
106
|
+
export function resetExpectedKeyCache() {
|
|
107
|
+
cachedExpectedKey = undefined;
|
|
108
|
+
}
|
|
109
|
+
/**
|
|
110
|
+
* Constant-time key comparison. `timingSafeEqual` throws on unequal-length
|
|
111
|
+
* buffers, so length is checked first (a length mismatch is an immediate,
|
|
112
|
+
* non-secret reject).
|
|
113
|
+
*/
|
|
114
|
+
export function keysMatch(presented, expected) {
|
|
115
|
+
const a = Buffer.from(presented, 'utf8');
|
|
116
|
+
const b = Buffer.from(expected, 'utf8');
|
|
117
|
+
if (a.length !== b.length)
|
|
118
|
+
return false;
|
|
119
|
+
return timingSafeEqual(a, b);
|
|
120
|
+
}
|
|
121
|
+
/**
|
|
122
|
+
* Routes intentionally reachable without the key. Kept deliberately narrow:
|
|
123
|
+
* pre-auth liveness/version probes, the Tower launcher shell, the React
|
|
124
|
+
* dashboard's static assets (the page loads keyless, then authenticates its
|
|
125
|
+
* own API/WebSocket calls with the key), the annotator shell + vendor files,
|
|
126
|
+
* and the cloud OAuth callback. Everything else requires the key.
|
|
127
|
+
*
|
|
128
|
+
* The entries here are the paths a *browser navigation* reaches — a top-level
|
|
129
|
+
* redirect, an iframe load, a `<script>`/`<link>` tag — none of which can
|
|
130
|
+
* attach the `codev-tower-key` header. Each one therefore carries either no
|
|
131
|
+
* secret or its own credential; adding a route here means proving that.
|
|
132
|
+
*
|
|
133
|
+
* The privileged workspace `file` reader and every `api/` or `ws/` subpath are
|
|
134
|
+
* explicitly excluded so a static-asset carve-out never exposes a data route.
|
|
135
|
+
*/
|
|
136
|
+
export function isPublicRoute(method, pathname) {
|
|
137
|
+
if (method !== 'GET')
|
|
138
|
+
return false;
|
|
139
|
+
if (pathname === '/health')
|
|
140
|
+
return true;
|
|
141
|
+
if (pathname === '/api/version')
|
|
142
|
+
return true;
|
|
143
|
+
if (pathname === '/' || pathname === '/index.html')
|
|
144
|
+
return true;
|
|
145
|
+
// Cloud OAuth callback (#1570). cloud.codevos.ai ends registration by
|
|
146
|
+
// redirecting the browser here, and a cross-site top-level navigation cannot
|
|
147
|
+
// carry the key header — so the key check can only ever 401 the flow. The
|
|
148
|
+
// route is self-authenticating instead: the nonce IS the credential. It is
|
|
149
|
+
// single-use with a 5-minute TTL and can only be minted by an authenticated
|
|
150
|
+
// POST /api/tunnel/connect, and handleTunnelEndpoint 400s ("Invalid or
|
|
151
|
+
// expired registration link") without a valid one, so a keyless request
|
|
152
|
+
// registers nothing. Exact match only — never widen this to an /api/tunnel/
|
|
153
|
+
// prefix, which would expose the keyed status/disconnect routes.
|
|
154
|
+
if (pathname === '/api/tunnel/connect/callback')
|
|
155
|
+
return true;
|
|
156
|
+
// React SPA served under /workspace/<encoded>/... — static assets only. The
|
|
157
|
+
// trailing subpath is optional: bare /workspace/<enc> serves the SPA shell,
|
|
158
|
+
// same as /workspace/<enc>/ (handleWorkspaceRoutes treats them identically).
|
|
159
|
+
const workspaceMatch = pathname.match(/^\/workspace\/[^/]+(?:\/(.*))?$/);
|
|
160
|
+
if (workspaceMatch) {
|
|
161
|
+
const subPath = workspaceMatch[1] || '';
|
|
162
|
+
// Annotator: its HTML shell and vendor libraries are loaded by iframe
|
|
163
|
+
// navigation and <script>/<link> tags that cannot carry the key header, so
|
|
164
|
+
// they are public (no secret; the shell gets the key injected same-origin).
|
|
165
|
+
// Every data/media sub-route (file, save, api/mtime, api/image, ...) stays
|
|
166
|
+
// keyed — the shell fetches them with the key.
|
|
167
|
+
const annotate = subPath.match(/^api\/annotate\/[^/]+(?:\/(.*))?$/);
|
|
168
|
+
if (annotate) {
|
|
169
|
+
const annotateSub = annotate[1] || '';
|
|
170
|
+
return annotateSub === '' || annotateSub.startsWith('vendor/');
|
|
171
|
+
}
|
|
172
|
+
if (subPath.startsWith('api/') || subPath === 'api')
|
|
173
|
+
return false;
|
|
174
|
+
if (subPath.startsWith('ws/') || subPath === 'ws')
|
|
175
|
+
return false;
|
|
176
|
+
if (subPath === 'file')
|
|
177
|
+
return false;
|
|
178
|
+
return true;
|
|
179
|
+
}
|
|
180
|
+
return false;
|
|
181
|
+
}
|
|
182
|
+
/**
|
|
183
|
+
* CORS origin allowlist (advisory Layer 3). Replaces the previous
|
|
184
|
+
* reflect-any-`https://` behavior. Allowed: loopback origins on any port
|
|
185
|
+
* (`http://localhost[:port]`, `http://127.0.0.1[:port]`) plus any origins an
|
|
186
|
+
* operator lists in `CODEV_TOWER_ALLOWED_ORIGINS` (comma-separated, exact
|
|
187
|
+
* match) for a tunnel/proxy deployment. Secure by default: no wildcard, no
|
|
188
|
+
* scheme-only reflection. CORS is defense-in-depth; the key check is the
|
|
189
|
+
* actual control.
|
|
190
|
+
*/
|
|
191
|
+
export function isAllowedOrigin(origin) {
|
|
192
|
+
if (/^http:\/\/localhost(:\d+)?$/.test(origin))
|
|
193
|
+
return true;
|
|
194
|
+
if (/^http:\/\/127\.0\.0\.1(:\d+)?$/.test(origin))
|
|
195
|
+
return true;
|
|
196
|
+
const configured = process.env.CODEV_TOWER_ALLOWED_ORIGINS;
|
|
197
|
+
if (configured) {
|
|
198
|
+
for (const allowed of configured.split(',')) {
|
|
199
|
+
if (allowed.trim() === origin)
|
|
200
|
+
return true;
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
return false;
|
|
204
|
+
}
|
|
205
|
+
/** Extract the hostname (no port) from a `Host` header value. */
|
|
206
|
+
function hostnameOf(hostHeader) {
|
|
207
|
+
const h = hostHeader.trim();
|
|
208
|
+
if (h.startsWith('[')) {
|
|
209
|
+
// IPv6 literal: [::1] or [::1]:port. The remainder after ] must be empty or
|
|
210
|
+
// a :port — otherwise the value is malformed and returned whole (matching
|
|
211
|
+
// nothing on the allowlist) rather than trusting its leading literal.
|
|
212
|
+
const end = h.indexOf(']');
|
|
213
|
+
if (end < 0)
|
|
214
|
+
return h;
|
|
215
|
+
const rest = h.slice(end + 1);
|
|
216
|
+
if (rest === '' || /^:\d+$/.test(rest))
|
|
217
|
+
return h.slice(1, end);
|
|
218
|
+
return h;
|
|
219
|
+
}
|
|
220
|
+
const colon = h.lastIndexOf(':');
|
|
221
|
+
if (colon >= 0 && /^\d+$/.test(h.slice(colon + 1))) {
|
|
222
|
+
return h.slice(0, colon);
|
|
223
|
+
}
|
|
224
|
+
return h;
|
|
225
|
+
}
|
|
226
|
+
/** True if `hostname` is an IPv4 or IPv6 literal (brackets already stripped). */
|
|
227
|
+
function isIpLiteral(hostname) {
|
|
228
|
+
if (/^\d{1,3}(\.\d{1,3}){3}$/.test(hostname))
|
|
229
|
+
return true; // IPv4
|
|
230
|
+
return hostname.includes(':'); // IPv6 (a DNS name never contains ':')
|
|
231
|
+
}
|
|
232
|
+
/** True if `hostname` matches a `CODEV_TOWER_ALLOWED_ORIGINS` entry's host. */
|
|
233
|
+
function isConfiguredHost(hostname) {
|
|
234
|
+
const configured = process.env.CODEV_TOWER_ALLOWED_ORIGINS;
|
|
235
|
+
if (!configured)
|
|
236
|
+
return false;
|
|
237
|
+
for (const origin of configured.split(',')) {
|
|
238
|
+
try {
|
|
239
|
+
if (new URL(origin.trim()).hostname.toLowerCase() === hostname)
|
|
240
|
+
return true;
|
|
241
|
+
}
|
|
242
|
+
catch { /* ignore malformed entry */ }
|
|
243
|
+
}
|
|
244
|
+
return false;
|
|
245
|
+
}
|
|
246
|
+
/**
|
|
247
|
+
* Host allowlist (advisory GHSA-xvjp-7748-v88v). A DNS-rebinding guard for the
|
|
248
|
+
* key-bearing dashboard shell: the key is injected into the (public) shell, so a
|
|
249
|
+
* browser rebound to Tower's address would carry the attacker's hostname in
|
|
250
|
+
* `Host` and be rejected before the shell is served. Allowed: loopback hostnames
|
|
251
|
+
* and the hostnames of any `CODEV_TOWER_ALLOWED_ORIGINS` (the same var the CORS
|
|
252
|
+
* allowlist uses, so a tunnel/proxy deployment configures both).
|
|
253
|
+
*
|
|
254
|
+
* In BRIDGE_MODE the operator has deliberately exposed Tower on the network, so
|
|
255
|
+
* the allowlist ALSO accepts IP-literal Hosts — a LAN client reaches Tower by IP
|
|
256
|
+
* (`http://192.168.1.5:4100/`), while DNS rebinding requires a host *name*, which
|
|
257
|
+
* stays rejected even in bridge mode. Hostname-based LAN access uses the
|
|
258
|
+
* `CODEV_TOWER_ALLOWED_ORIGINS` escape hatch. The key (mandatory under bridge
|
|
259
|
+
* mode) remains the primary control and is checked separately for keyed routes;
|
|
260
|
+
* this relaxation never weakens it.
|
|
261
|
+
*/
|
|
262
|
+
export function isAllowedHost(hostHeader) {
|
|
263
|
+
if (!hostHeader)
|
|
264
|
+
return false;
|
|
265
|
+
const hostname = hostnameOf(hostHeader).toLowerCase();
|
|
266
|
+
if (hostname === 'localhost' || hostname === '127.0.0.1' || hostname === '::1')
|
|
267
|
+
return true;
|
|
268
|
+
if (isConfiguredHost(hostname))
|
|
269
|
+
return true;
|
|
270
|
+
if (process.env.BRIDGE_MODE === '1' && isIpLiteral(hostname))
|
|
271
|
+
return true;
|
|
272
|
+
return false;
|
|
273
|
+
}
|
|
274
|
+
/** Read one header value as a non-empty string, or null. */
|
|
275
|
+
function headerValue(raw) {
|
|
276
|
+
if (typeof raw === 'string' && raw.length > 0)
|
|
277
|
+
return raw;
|
|
278
|
+
if (Array.isArray(raw) && raw.length > 0 && raw[0])
|
|
279
|
+
return raw[0];
|
|
280
|
+
return null;
|
|
281
|
+
}
|
|
282
|
+
/**
|
|
283
|
+
* Read the key a client presented on an HTTP request, or null if absent. Prefers
|
|
284
|
+
* the current `codev-tower-key` header and falls back to the legacy
|
|
285
|
+
* `codev-web-key` (dual-accept for one release) so an already-installed client
|
|
286
|
+
* bundling an older sdk still authenticates while it updates.
|
|
287
|
+
*/
|
|
288
|
+
function presentedHttpKey(req) {
|
|
289
|
+
return headerValue(req.headers[TOWER_KEY_HEADER]) ?? headerValue(req.headers[LEGACY_WEB_KEY_HEADER]);
|
|
290
|
+
}
|
|
291
|
+
/**
|
|
292
|
+
* Security: decide whether an HTTP request may proceed.
|
|
293
|
+
*
|
|
294
|
+
* Public-allowlisted routes pass keyless; every other route must present a
|
|
295
|
+
* `codev-tower-key` header that constant-time-matches the expected local key.
|
|
296
|
+
* Fails closed when the expected key is unavailable.
|
|
297
|
+
*
|
|
69
298
|
* @param req - HTTP incoming message
|
|
70
|
-
* @returns true
|
|
299
|
+
* @returns true if the request is authorized
|
|
300
|
+
*/
|
|
301
|
+
export function isRequestAllowed(req) {
|
|
302
|
+
// Host guard runs first (even for public routes) — the key is injected into the
|
|
303
|
+
// public dashboard shell, so a rebound Host must not reach it.
|
|
304
|
+
if (!isAllowedHost(req.headers.host))
|
|
305
|
+
return false;
|
|
306
|
+
const method = req.method || 'GET';
|
|
307
|
+
let pathname = '/';
|
|
308
|
+
try {
|
|
309
|
+
pathname = new URL(req.url || '/', 'http://localhost').pathname;
|
|
310
|
+
}
|
|
311
|
+
catch {
|
|
312
|
+
return false;
|
|
313
|
+
}
|
|
314
|
+
if (isPublicRoute(method, pathname))
|
|
315
|
+
return true;
|
|
316
|
+
const expected = getExpectedKey();
|
|
317
|
+
if (!expected)
|
|
318
|
+
return false;
|
|
319
|
+
const presented = presentedHttpKey(req);
|
|
320
|
+
if (!presented)
|
|
321
|
+
return false;
|
|
322
|
+
return keysMatch(presented, expected);
|
|
323
|
+
}
|
|
324
|
+
/**
|
|
325
|
+
* Extract the presented key from a WebSocket upgrade's `Sec-WebSocket-Protocol`
|
|
326
|
+
* offer (the `codev-key.<KEY>` token), or null if absent/malformed.
|
|
327
|
+
*/
|
|
328
|
+
function presentedWsKey(req) {
|
|
329
|
+
const raw = req.headers['sec-websocket-protocol'];
|
|
330
|
+
if (!raw)
|
|
331
|
+
return null;
|
|
332
|
+
const offered = (Array.isArray(raw) ? raw.join(',') : raw)
|
|
333
|
+
.split(',')
|
|
334
|
+
.map((p) => p.trim());
|
|
335
|
+
for (const proto of offered) {
|
|
336
|
+
if (proto.startsWith(WS_KEY_PROTOCOL_PREFIX)) {
|
|
337
|
+
const key = proto.slice(WS_KEY_PROTOCOL_PREFIX.length);
|
|
338
|
+
return key.length > 0 ? key : null;
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
return null;
|
|
342
|
+
}
|
|
343
|
+
/**
|
|
344
|
+
* The subprotocol the server echoes back on a terminal WebSocket handshake:
|
|
345
|
+
* the non-secret marker if the client offered it, else none. NEVER echoes the
|
|
346
|
+
* `codev-key.<key>` token — echoing it would leak the key into a response
|
|
347
|
+
* header. Used as the `ws` server's `handleProtocols` (advisory
|
|
348
|
+
* GHSA-xvjp-7748-v88v). Exported so the echo contract is unit-testable.
|
|
349
|
+
*/
|
|
350
|
+
export function selectWsSubprotocol(offered) {
|
|
351
|
+
return offered.has(WS_MARKER_PROTOCOL) ? WS_MARKER_PROTOCOL : false;
|
|
352
|
+
}
|
|
353
|
+
/**
|
|
354
|
+
* Security: decide whether a WebSocket upgrade may proceed. Validated at the
|
|
355
|
+
* handshake (before any PTY attach), independent of the `Origin` header so a
|
|
356
|
+
* missing Origin can never degrade into an auth bypass. Fails closed when the
|
|
357
|
+
* expected key is unavailable.
|
|
358
|
+
*
|
|
359
|
+
* @param req - HTTP upgrade request
|
|
360
|
+
* @returns true if the upgrade is authorized
|
|
71
361
|
*/
|
|
72
|
-
export function
|
|
73
|
-
|
|
362
|
+
export function isWebSocketAllowed(req) {
|
|
363
|
+
if (!isAllowedHost(req.headers.host))
|
|
364
|
+
return false;
|
|
365
|
+
const expected = getExpectedKey();
|
|
366
|
+
if (!expected)
|
|
367
|
+
return false;
|
|
368
|
+
const presented = presentedWsKey(req);
|
|
369
|
+
if (!presented)
|
|
370
|
+
return false;
|
|
371
|
+
return keysMatch(presented, expected);
|
|
74
372
|
}
|
|
75
373
|
/**
|
|
76
374
|
* Validate a bind host value for server.listen().
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"server-utils.js","sourceRoot":"","sources":["../../../src/agent-farm/utils/server-utils.ts"],"names":[],"mappings":"AAAA;;;;GAIG;
|
|
1
|
+
{"version":3,"file":"server-utils.js","sourceRoot":"","sources":["../../../src/agent-farm/utils/server-utils.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Context-refresh boundary resolution and task emission (Spec 1470).
|
|
3
|
+
*
|
|
4
|
+
* ## What a boundary is, and why it is recorded rather than inferred
|
|
5
|
+
*
|
|
6
|
+
* A boundary is a moment porch already transitions the state machine, at which a
|
|
7
|
+
* builder's context can be discarded without losing anything: the spec, the plan,
|
|
8
|
+
* `status.yaml`, the thread narrative and git carry the durable state, so a
|
|
9
|
+
* refreshed builder re-orients from disk rather than from memory.
|
|
10
|
+
*
|
|
11
|
+
* The refresh is DESTRUCTIVE — it ends in `/clear`, which has no undo — so it must
|
|
12
|
+
* fire at most once per boundary. Porch transitions can loop: issue #1408 saw
|
|
13
|
+
* verify-approval reset every plan phase back to `pending`, which would replay
|
|
14
|
+
* every plan-phase advance. A side effect that wipes context cannot be guarded by
|
|
15
|
+
* inferring "we must already have done this" from phase or iteration; it has to be
|
|
16
|
+
* a RECORDED FACT. That is what `context_refreshes` in `status.yaml` is.
|
|
17
|
+
*
|
|
18
|
+
* ## Where at-most-once actually comes from
|
|
19
|
+
*
|
|
20
|
+
* Not from a lock or a guard, but from control flow. Each transition site mutates
|
|
21
|
+
* the phase fields AND appends the boundary record in ONE `writeStateAndCommit`,
|
|
22
|
+
* then returns the refresh task INSTEAD of recursing into `next()`. The next
|
|
23
|
+
* `porch next` reads a state where the boundary is already recorded, so
|
|
24
|
+
* {@link hasRefreshed} is true and the normal path runs. There is no window in
|
|
25
|
+
* which the record and the transition disagree, because they are the same write.
|
|
26
|
+
*
|
|
27
|
+
* ## Failure semantics: consumed means consumed
|
|
28
|
+
*
|
|
29
|
+
* The boundary is recorded when the task is EMITTED, not when the refresh
|
|
30
|
+
* succeeds. A refresh that fails, or that the builder never runs, is never
|
|
31
|
+
* retried. That is deliberate:
|
|
32
|
+
*
|
|
33
|
+
* - The builder-side command never writes `status.yaml` (only porch does), so
|
|
34
|
+
* there is no completion signal to model and no way for a failed refresh to
|
|
35
|
+
* corrupt protocol state.
|
|
36
|
+
* - A missed refresh costs context and nothing else. A repeated one costs a
|
|
37
|
+
* builder's working memory, over and over, with nobody watching.
|
|
38
|
+
*
|
|
39
|
+
* Asymmetric costs, so the design fails toward doing nothing.
|
|
40
|
+
*/
|
|
41
|
+
import type { PorchTask, ProjectState, Protocol } from './types.js';
|
|
42
|
+
/**
|
|
43
|
+
* Identifier for entering a protocol phase.
|
|
44
|
+
*
|
|
45
|
+
* Derived from the transition rather than from a configured label, so the record
|
|
46
|
+
* in `status.yaml` cannot drift from the event that produced it.
|
|
47
|
+
*/
|
|
48
|
+
export declare function enterBoundary(phaseId: string): string;
|
|
49
|
+
/** Identifier for advancing INTO a given plan phase. */
|
|
50
|
+
export declare function planPhaseBoundary(planPhaseId: string): string;
|
|
51
|
+
/**
|
|
52
|
+
* Is a phase-entry boundary declared for this protocol?
|
|
53
|
+
*
|
|
54
|
+
* Inspects the FIELDS, never the presence of the `context_refresh` object.
|
|
55
|
+
* `context_refresh: {}` is valid and declares nothing, so a truthiness check on
|
|
56
|
+
* the object would report every boundary as declared for a protocol that opted
|
|
57
|
+
* into none of them.
|
|
58
|
+
*/
|
|
59
|
+
export declare function declaresEnter(protocol: Protocol, phaseId: string): boolean;
|
|
60
|
+
/** Is the plan-phase-advance boundary declared for this protocol? */
|
|
61
|
+
export declare function declaresPlanPhaseAdvance(protocol: Protocol): boolean;
|
|
62
|
+
/** Has this boundary already been refreshed for this project? */
|
|
63
|
+
export declare function hasRefreshed(state: ProjectState, boundary: string): boolean;
|
|
64
|
+
/**
|
|
65
|
+
* Record a boundary on the state object.
|
|
66
|
+
*
|
|
67
|
+
* Mutates rather than writing: the caller folds this into the SAME
|
|
68
|
+
* `writeStateAndCommit` as the transition itself, which is what makes the record
|
|
69
|
+
* and the transition atomic. A separate write here would open exactly the window
|
|
70
|
+
* this design closes.
|
|
71
|
+
*/
|
|
72
|
+
export declare function recordRefresh(state: ProjectState, boundary: string, now: string): void;
|
|
73
|
+
/**
|
|
74
|
+
* Mark every recorded boundary as acknowledged, returning true if anything changed.
|
|
75
|
+
*
|
|
76
|
+
* ## Why an acknowledgment exists at all
|
|
77
|
+
*
|
|
78
|
+
* A refresh is unattended, and its worst failure is silent: the builder clears,
|
|
79
|
+
* the re-entry never arrives, and nothing ever says so. The plan's first design
|
|
80
|
+
* tried to DERIVE that from `updated_at` — but the normal task-emission path in
|
|
81
|
+
* `next()` writes no state, so `updated_at` stays pinned at the transition for
|
|
82
|
+
* the whole of a healthy build. A threshold long enough to avoid false positives
|
|
83
|
+
* would be far too long to catch the stall it exists for.
|
|
84
|
+
*
|
|
85
|
+
* So porch records the fact instead. This runs on the first `porch next` that
|
|
86
|
+
* reaches the NORMAL path after a boundary — which only happens if a builder came
|
|
87
|
+
* back and asked for work. A boundary recorded but never acknowledged therefore
|
|
88
|
+
* means exactly one thing: nobody returned.
|
|
89
|
+
*
|
|
90
|
+
* ## Why porch owns it
|
|
91
|
+
*
|
|
92
|
+
* The builder-side command is forbidden from writing `status.yaml`, and that
|
|
93
|
+
* prohibition is load-bearing — it is what makes a failed refresh incapable of
|
|
94
|
+
* corrupting protocol state. So the acknowledgment cannot come from the side that
|
|
95
|
+
* would most naturally report "I'm back"; it has to be inferred by the only
|
|
96
|
+
* writer, from the only evidence it has: the builder asked for its next task.
|
|
97
|
+
*
|
|
98
|
+
* Writes at most once per boundary, so this costs one extra state write per
|
|
99
|
+
* refresh rather than one per `porch next`.
|
|
100
|
+
*
|
|
101
|
+
* ## What this cannot distinguish, and why that is accepted
|
|
102
|
+
*
|
|
103
|
+
* Any normal-path pass acknowledges EVERY outstanding boundary. So a refresh
|
|
104
|
+
* that was refused before clearing — after which the builder simply runs `porch
|
|
105
|
+
* next` — reads as acknowledged, as does a stall a human recovered by hand.
|
|
106
|
+
*
|
|
107
|
+
* That is not a bug to fix here, because porch has exactly one piece of
|
|
108
|
+
* evidence: the builder asked for work. It cannot tell "cleared and came back"
|
|
109
|
+
* from "never cleared" from "was rescued", and inventing a distinction it cannot
|
|
110
|
+
* observe would be worse than admitting the limit. What the signal genuinely
|
|
111
|
+
* means is narrow and still useful: **no builder has asked for work since this
|
|
112
|
+
* boundary was recorded.**
|
|
113
|
+
*/
|
|
114
|
+
export declare function acknowledgeRefreshes(state: ProjectState, now: string): boolean;
|
|
115
|
+
/**
|
|
116
|
+
* How long a refresh may legitimately take before silence means trouble.
|
|
117
|
+
*
|
|
118
|
+
* A refresh is not instant: the builder writes a save (minutes, for a real one),
|
|
119
|
+
* the re-entry is held by Tower for {@link DEFAULT_REENTRY_DELAY_SECONDS}, the
|
|
120
|
+
* mailbox waits for a clean prompt, and only then does the builder run `porch
|
|
121
|
+
* next`. Ten minutes is comfortably past all of that and still far sooner than a
|
|
122
|
+
* human would otherwise notice a builder sitting cleared and idle.
|
|
123
|
+
*/
|
|
124
|
+
export declare const REFRESH_STALL_GRACE_MS: number;
|
|
125
|
+
/**
|
|
126
|
+
* Boundaries recorded but never acknowledged — the raw fact, no timing applied.
|
|
127
|
+
*
|
|
128
|
+
* Use this for HISTORY (what has and has not been acknowledged). For deciding
|
|
129
|
+
* whether to warn a human, use {@link stalledRefreshes}, which adds the grace
|
|
130
|
+
* period.
|
|
131
|
+
*/
|
|
132
|
+
export declare function unacknowledgedRefreshes(state: ProjectState): Array<{
|
|
133
|
+
boundary: string;
|
|
134
|
+
at: string;
|
|
135
|
+
}>;
|
|
136
|
+
/**
|
|
137
|
+
* Unacknowledged boundaries old enough that silence is a problem.
|
|
138
|
+
*
|
|
139
|
+
* ## Why a grace period after all
|
|
140
|
+
*
|
|
141
|
+
* An earlier version of this warned on ANY unacknowledged boundary, with a
|
|
142
|
+
* comment arguing that time-based rules do not work here. That argument
|
|
143
|
+
* conflated two different things, and the result was a warning on every healthy
|
|
144
|
+
* refresh — a false positive during completely normal operation, every single
|
|
145
|
+
* time.
|
|
146
|
+
*
|
|
147
|
+
* What genuinely does not work is deriving the stall from `updated_at`, because
|
|
148
|
+
* that timestamp does not move during a healthy build. But the ACKNOWLEDGMENT
|
|
149
|
+
* does move, exactly when the builder returns. Given a reliable event, a grace
|
|
150
|
+
* period is simply the answer to "has enough time passed that silence is
|
|
151
|
+
* suspicious?" — which is the question, and which the plan asked for in the
|
|
152
|
+
* words "past a threshold".
|
|
153
|
+
*
|
|
154
|
+
* A signal that fires during normal operation is not a signal.
|
|
155
|
+
*/
|
|
156
|
+
export declare function stalledRefreshes(state: ProjectState, now: number, graceMs?: number): Array<{
|
|
157
|
+
boundary: string;
|
|
158
|
+
at: string;
|
|
159
|
+
ageMs: number;
|
|
160
|
+
}>;
|
|
161
|
+
/**
|
|
162
|
+
* Should a refresh fire for `boundary`, and is it declared?
|
|
163
|
+
*
|
|
164
|
+
* Deliberately does NOT consider how much context the builder has used. The spec
|
|
165
|
+
* fixes always-fire-at-configured-boundaries over threshold triggering:
|
|
166
|
+
* thresholds would need harness introspection porch does not have, and would make
|
|
167
|
+
* the behavior non-deterministic and untestable. An unnecessary refresh at a
|
|
168
|
+
* clean boundary costs a re-read of artifacts the builder can already see.
|
|
169
|
+
*/
|
|
170
|
+
export declare function shouldRefresh(state: ProjectState, declared: boolean, boundary: string): boolean;
|
|
171
|
+
/**
|
|
172
|
+
* The single task emitted at a boundary.
|
|
173
|
+
*
|
|
174
|
+
* Deliberately does NOT instruct `porch done`. A refresh is not a build: `porch
|
|
175
|
+
* done` would validate checks and advance the build state, which is wrong at a
|
|
176
|
+
* moment when nothing was built. The builder refreshes and then re-enters through
|
|
177
|
+
* `porch next`, which is also what the post-clear re-entry frame says — so both
|
|
178
|
+
* paths converge on the same next command whether or not the clear lands.
|
|
179
|
+
*/
|
|
180
|
+
export declare function buildRefreshTask(boundary: string): PorchTask;
|
|
181
|
+
//# sourceMappingURL=context-refresh.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"context-refresh.d.ts","sourceRoot":"","sources":["../../../src/commands/porch/context-refresh.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAuCG;AAIH,OAAO,KAAK,EAAwB,SAAS,EAAE,YAAY,EAAE,QAAQ,EAAE,MAAM,YAAY,CAAC;AAM1F;;;;;GAKG;AACH,wBAAgB,aAAa,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,CAErD;AAED,wDAAwD;AACxD,wBAAgB,iBAAiB,CAAC,WAAW,EAAE,MAAM,GAAG,MAAM,CAE7D;AAMD;;;;;;;GAOG;AACH,wBAAgB,aAAa,CAAC,QAAQ,EAAE,QAAQ,EAAE,OAAO,EAAE,MAAM,GAAG,OAAO,CAG1E;AAED,qEAAqE;AACrE,wBAAgB,wBAAwB,CAAC,QAAQ,EAAE,QAAQ,GAAG,OAAO,CAEpE;AAMD,iEAAiE;AACjE,wBAAgB,YAAY,CAAC,KAAK,EAAE,YAAY,EAAE,QAAQ,EAAE,MAAM,GAAG,OAAO,CAE3E;AAED;;;;;;;GAOG;AACH,wBAAgB,aAAa,CAAC,KAAK,EAAE,YAAY,EAAE,QAAQ,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,GAAG,IAAI,CAItF;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAwCG;AACH,wBAAgB,oBAAoB,CAAC,KAAK,EAAE,YAAY,EAAE,GAAG,EAAE,MAAM,GAAG,OAAO,CAS9E;AAED;;;;;;;;GAQG;AACH,eAAO,MAAM,sBAAsB,QAAiB,CAAC;AAErD;;;;;;GAMG;AACH,wBAAgB,uBAAuB,CACrC,KAAK,EAAE,YAAY,GAClB,KAAK,CAAC;IAAE,QAAQ,EAAE,MAAM,CAAC;IAAC,EAAE,EAAE,MAAM,CAAA;CAAE,CAAC,CAIzC;AAED;;;;;;;;;;;;;;;;;;;GAmBG;AACH,wBAAgB,gBAAgB,CAC9B,KAAK,EAAE,YAAY,EACnB,GAAG,EAAE,MAAM,EACX,OAAO,SAAyB,GAC/B,KAAK,CAAC;IAAE,QAAQ,EAAE,MAAM,CAAC;IAAC,EAAE,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,MAAM,CAAA;CAAE,CAAC,CAOxD;AAMD;;;;;;;;GAQG;AACH,wBAAgB,aAAa,CAC3B,KAAK,EAAE,YAAY,EACnB,QAAQ,EAAE,OAAO,EACjB,QAAQ,EAAE,MAAM,GACf,OAAO,CAET;AAMD;;;;;;;;GAQG;AACH,wBAAgB,gBAAgB,CAAC,QAAQ,EAAE,MAAM,GAAG,SAAS,CA0C5D"}
|