@crouton-kit/crouter 0.3.40 → 0.3.42
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/dist/build-root.js +1 -0
- package/dist/builtin-pi-packages/pi-crtr-extensions/extensions/__tests__/provider-rotation.test.ts +472 -9
- package/dist/builtin-pi-packages/pi-crtr-extensions/extensions/provider-rotation.ts +284 -76
- package/dist/builtin-pi-packages/pi-crtr-extensions/extensions/strip-skills-docs.ts +31 -24
- package/dist/builtin-pi-packages/pi-crtr-extensions/lib/subscription-state.ts +52 -15
- package/dist/clients/attach/attach-cmd.js +720 -747
- package/dist/clients/attach/chat-view.js +11 -0
- package/dist/clients/attach/view-socket.d.ts +8 -1
- package/dist/clients/attach/view-socket.js +15 -1
- package/dist/clients/web/server.js +5 -28
- package/dist/clients/web/web-cmd.js +1 -1
- package/dist/commands/__tests__/revive-now-gate.test.js +60 -0
- package/dist/commands/canvas-rebuild-index.js +5 -5
- package/dist/commands/memory/shared.d.ts +7 -3
- package/dist/commands/memory/shared.js +35 -5
- package/dist/commands/memory/write.js +5 -3
- package/dist/commands/node-snapshot.js +9 -1
- package/dist/commands/node.js +37 -17
- package/dist/commands/push.js +8 -0
- package/dist/commands/revive.d.ts +10 -0
- package/dist/commands/revive.js +28 -10
- package/dist/commands/sys/__tests__/setup-core.test.js +19 -0
- package/dist/commands/sys/doctor.js +1 -1
- package/dist/commands/sys/setup-core.js +3 -2
- package/dist/commands/sys/setup.js +1 -1
- package/dist/commands/worktree.d.ts +2 -0
- package/dist/commands/worktree.js +94 -0
- package/dist/core/__tests__/boot.test.js +4 -4
- package/dist/core/__tests__/canvas.test.js +19 -7
- package/dist/core/__tests__/child-followup.test.js +15 -5
- package/dist/core/__tests__/daemon-boot.test.js +6 -1
- package/dist/core/__tests__/daemon-wedge.test.js +18 -1
- package/dist/core/__tests__/fault-classifier.test.js +30 -0
- package/dist/core/__tests__/fixtures/fake-engine.d.ts +13 -0
- package/dist/core/__tests__/fixtures/fake-engine.js +20 -0
- package/dist/core/__tests__/full/spike-harness.test.js +8 -5
- package/dist/core/__tests__/grace-clock.test.js +18 -2
- package/dist/core/__tests__/host-teardown-process-group.test.js +465 -0
- package/dist/core/__tests__/prune-to-limit.test.js +14 -0
- package/dist/core/__tests__/review-model-floor.test.js +32 -0
- package/dist/core/__tests__/revive.test.js +18 -4
- package/dist/core/__tests__/session-cycles.test.js +77 -0
- package/dist/core/__tests__/worktree.test.js +85 -0
- package/dist/core/canvas/boot.js +12 -7
- package/dist/core/canvas/canvas.d.ts +27 -8
- package/dist/core/canvas/canvas.js +54 -26
- package/dist/core/canvas/db.js +14 -0
- package/dist/core/canvas/history.js +1 -0
- package/dist/core/canvas/paths.d.ts +10 -9
- package/dist/core/canvas/paths.js +10 -9
- package/dist/core/canvas/pid.d.ts +155 -1
- package/dist/core/canvas/pid.js +306 -1
- package/dist/core/canvas/status-glyph.d.ts +7 -0
- package/dist/core/canvas/status-glyph.js +10 -1
- package/dist/core/canvas/types.d.ts +34 -0
- package/dist/core/fault-classifier.js +5 -1
- package/dist/core/profiles/select.d.ts +4 -2
- package/dist/core/profiles/select.js +30 -4
- package/dist/core/runtime/bearings.js +4 -0
- package/dist/core/runtime/branded-host.d.ts +7 -0
- package/dist/core/runtime/branded-host.js +44 -17
- package/dist/core/runtime/broker-sdk.js +28 -68
- package/dist/core/runtime/broker.js +35 -4
- package/dist/core/runtime/host.d.ts +3 -3
- package/dist/core/runtime/host.js +148 -33
- package/dist/core/runtime/launch.d.ts +15 -15
- package/dist/core/runtime/launch.js +58 -4
- package/dist/core/runtime/naming.js +3 -2
- package/dist/core/runtime/nodes.d.ts +3 -1
- package/dist/core/runtime/nodes.js +1 -0
- package/dist/core/runtime/pi-cli.d.ts +2 -0
- package/dist/core/runtime/pi-cli.js +51 -0
- package/dist/core/runtime/placement.d.ts +1 -1
- package/dist/core/runtime/placement.js +5 -2
- package/dist/core/runtime/recap.js +2 -1
- package/dist/core/runtime/revive.d.ts +21 -15
- package/dist/core/runtime/revive.js +87 -52
- package/dist/core/runtime/session-cycles.d.ts +30 -0
- package/dist/core/runtime/session-cycles.js +77 -0
- package/dist/core/runtime/spawn.d.ts +4 -0
- package/dist/core/runtime/spawn.js +152 -100
- package/dist/core/runtime/tmux.d.ts +7 -10
- package/dist/core/runtime/tmux.js +9 -11
- package/dist/core/worktree.d.ts +35 -0
- package/dist/core/worktree.js +158 -0
- package/dist/daemon/crtrd.d.ts +17 -8
- package/dist/daemon/crtrd.js +191 -40
- package/dist/index.d.ts +3 -0
- package/dist/index.js +7 -0
- package/dist/pi-extensions/canvas-context-intro.d.ts +1 -0
- package/dist/pi-extensions/canvas-context-intro.js +34 -23
- package/dist/pi-extensions/canvas-stophook.js +11 -5
- package/dist/web-client/assets/{index-CbO8L0mN.js → index-B00YpRQ1.js} +20 -20
- package/dist/web-client/assets/index-DrkcvANq.css +2 -0
- package/dist/web-client/index.html +2 -2
- package/docs/compat/hearth-crtr-v1.md +191 -0
- package/docs/public-api.md +75 -0
- package/package.json +3 -4
- package/dist/core/__tests__/hearth-bootstrap.test.js +0 -136
- package/dist/core/hearth/__tests__/model-auth-guest.test.js +0 -151
- package/dist/core/hearth/config.d.ts +0 -3
- package/dist/core/hearth/config.js +0 -108
- package/dist/core/hearth/guest-env.d.ts +0 -9
- package/dist/core/hearth/guest-env.js +0 -27
- package/dist/core/hearth/index.d.ts +0 -4
- package/dist/core/hearth/index.js +0 -4
- package/dist/core/hearth/model-auth-guest.d.ts +0 -8
- package/dist/core/hearth/model-auth-guest.js +0 -430
- package/dist/core/hearth/provider.d.ts +0 -36
- package/dist/core/hearth/provider.js +0 -10
- package/dist/core/hearth/providers/__tests__/sweep-and-release.test.js +0 -362
- package/dist/core/hearth/providers/blaxel-bootstrap.d.ts +0 -12
- package/dist/core/hearth/providers/blaxel-bootstrap.js +0 -147
- package/dist/core/hearth/providers/blaxel-home.d.ts +0 -60
- package/dist/core/hearth/providers/blaxel-home.js +0 -405
- package/dist/core/hearth/providers/blaxel.d.ts +0 -36
- package/dist/core/hearth/providers/blaxel.js +0 -364
- package/dist/core/hearth/providers/types.d.ts +0 -93
- package/dist/core/hearth/types.d.ts +0 -155
- package/dist/hearth/control-plane/__tests__/error-serialization.test.js +0 -29
- package/dist/hearth/control-plane/__tests__/node-message.test.js +0 -60
- package/dist/hearth/control-plane/__tests__/oauth-serving-marker.test.d.ts +0 -1
- package/dist/hearth/control-plane/__tests__/oauth-serving-marker.test.js +0 -44
- package/dist/hearth/control-plane/__tests__/rate-limit-recurrence.test.d.ts +0 -1
- package/dist/hearth/control-plane/__tests__/rate-limit-recurrence.test.js +0 -49
- package/dist/hearth/control-plane/__tests__/relay-security.test.d.ts +0 -1
- package/dist/hearth/control-plane/__tests__/relay-security.test.js +0 -314
- package/dist/hearth/control-plane/__tests__/scheduler-scan-loop.test.d.ts +0 -1
- package/dist/hearth/control-plane/__tests__/scheduler-scan-loop.test.js +0 -133
- package/dist/hearth/control-plane/__tests__/trigger-delivery.test.d.ts +0 -1
- package/dist/hearth/control-plane/__tests__/trigger-delivery.test.js +0 -170
- package/dist/hearth/control-plane/__tests__/wake-roll.test.d.ts +0 -1
- package/dist/hearth/control-plane/__tests__/wake-roll.test.js +0 -230
- package/dist/hearth/control-plane/__tests__/webhook-ingress.test.d.ts +0 -1
- package/dist/hearth/control-plane/__tests__/webhook-ingress.test.js +0 -167
- package/dist/hearth/control-plane/config.d.ts +0 -21
- package/dist/hearth/control-plane/config.js +0 -77
- package/dist/hearth/control-plane/db.d.ts +0 -30
- package/dist/hearth/control-plane/db.js +0 -561
- package/dist/hearth/control-plane/hearth-target.d.ts +0 -23
- package/dist/hearth/control-plane/hearth-target.js +0 -68
- package/dist/hearth/control-plane/ingress/rate-limit.d.ts +0 -24
- package/dist/hearth/control-plane/ingress/rate-limit.js +0 -100
- package/dist/hearth/control-plane/ingress/route-store.d.ts +0 -31
- package/dist/hearth/control-plane/ingress/route-store.js +0 -61
- package/dist/hearth/control-plane/ingress/webhook-delivery-store.d.ts +0 -41
- package/dist/hearth/control-plane/ingress/webhook-delivery-store.js +0 -69
- package/dist/hearth/control-plane/ingress/webhook-route.d.ts +0 -55
- package/dist/hearth/control-plane/ingress/webhook-route.js +0 -285
- package/dist/hearth/control-plane/main.d.ts +0 -1
- package/dist/hearth/control-plane/main.js +0 -88
- package/dist/hearth/control-plane/node-message.d.ts +0 -31
- package/dist/hearth/control-plane/node-message.js +0 -98
- package/dist/hearth/control-plane/register.d.ts +0 -15
- package/dist/hearth/control-plane/register.js +0 -34
- package/dist/hearth/control-plane/registry.d.ts +0 -22
- package/dist/hearth/control-plane/registry.js +0 -168
- package/dist/hearth/control-plane/relay.d.ts +0 -44
- package/dist/hearth/control-plane/relay.js +0 -711
- package/dist/hearth/control-plane/scheduler/fire-store.d.ts +0 -36
- package/dist/hearth/control-plane/scheduler/fire-store.js +0 -73
- package/dist/hearth/control-plane/scheduler/recurrence.d.ts +0 -7
- package/dist/hearth/control-plane/scheduler/recurrence.js +0 -58
- package/dist/hearth/control-plane/scheduler/scan-loop.d.ts +0 -38
- package/dist/hearth/control-plane/scheduler/scan-loop.js +0 -138
- package/dist/hearth/control-plane/scheduler/schedule-store.d.ts +0 -32
- package/dist/hearth/control-plane/scheduler/schedule-store.js +0 -66
- package/dist/hearth/control-plane/secrets.d.ts +0 -31
- package/dist/hearth/control-plane/secrets.js +0 -134
- package/dist/hearth/control-plane/server.d.ts +0 -27
- package/dist/hearth/control-plane/server.js +0 -482
- package/dist/hearth/control-plane/serving.d.ts +0 -15
- package/dist/hearth/control-plane/serving.js +0 -106
- package/dist/hearth/control-plane/session.d.ts +0 -68
- package/dist/hearth/control-plane/session.js +0 -273
- package/dist/hearth/control-plane/triggers/acl.d.ts +0 -14
- package/dist/hearth/control-plane/triggers/acl.js +0 -52
- package/dist/hearth/control-plane/triggers/audit-store.d.ts +0 -38
- package/dist/hearth/control-plane/triggers/audit-store.js +0 -79
- package/dist/hearth/control-plane/triggers/deliver.d.ts +0 -43
- package/dist/hearth/control-plane/triggers/deliver.js +0 -76
- package/dist/hearth/control-plane/triggers/envelope.d.ts +0 -29
- package/dist/hearth/control-plane/triggers/envelope.js +0 -38
- package/dist/hearth/control-plane/types.d.ts +0 -86
- package/dist/hearth/control-plane/types.js +0 -1
- package/dist/hearth/control-plane/wake.d.ts +0 -86
- package/dist/hearth/control-plane/wake.js +0 -550
- package/dist/web-client/assets/index-DwO46Cs5.css +0 -2
- /package/dist/{core/__tests__/hearth-bootstrap.test.d.ts → commands/__tests__/revive-now-gate.test.d.ts} +0 -0
- /package/dist/{core/hearth/__tests__/model-auth-guest.test.d.ts → commands/sys/__tests__/setup-core.test.d.ts} +0 -0
- /package/dist/core/{hearth/providers/__tests__/sweep-and-release.test.d.ts → __tests__/fault-classifier.test.d.ts} +0 -0
- /package/dist/core/{hearth/providers/types.js → __tests__/host-teardown-process-group.test.d.ts} +0 -0
- /package/dist/core/{hearth/types.js → __tests__/review-model-floor.test.d.ts} +0 -0
- /package/dist/{hearth/control-plane/__tests__/error-serialization.test.d.ts → core/__tests__/session-cycles.test.d.ts} +0 -0
- /package/dist/{hearth/control-plane/__tests__/node-message.test.d.ts → core/__tests__/worktree.test.d.ts} +0 -0
|
@@ -1,136 +0,0 @@
|
|
|
1
|
-
import { test } from 'node:test';
|
|
2
|
-
import assert from 'node:assert/strict';
|
|
3
|
-
import { buildBootstrapScript } from '../hearth/providers/blaxel-bootstrap.js';
|
|
4
|
-
const FIRST_BOOT_OPTS = {
|
|
5
|
-
guestHome: '/home/agent',
|
|
6
|
-
crtrVersion: '0.3.33',
|
|
7
|
-
relayToken: 'relay-token',
|
|
8
|
-
webPort: 7878,
|
|
9
|
-
piAuthJson: '{"anthropic":{"type":"oauth","refresh":"r","access":"a","expires":1}}',
|
|
10
|
-
piPackages: ['npm:pi-claude-oauth-adapter@0.1.2'],
|
|
11
|
-
modelLaddersJson: '{"modelLadders":{"anthropic":{"strong":"anthropic/claude-opus-4-8:high"},"openai":{"strong":"openai-codex/gpt-5.5:high"},"defaultProvider":"anthropic"}}',
|
|
12
|
-
isFirstProvision: true,
|
|
13
|
-
};
|
|
14
|
-
function indexOf(script, needle) {
|
|
15
|
-
const i = script.indexOf(needle);
|
|
16
|
-
assert.ok(i !== -1, `expected to find ${JSON.stringify(needle)} in script`);
|
|
17
|
-
return i;
|
|
18
|
-
}
|
|
19
|
-
test('first provision seeds config/ladders and pi settings, never the rotation pool, and never installs', () => {
|
|
20
|
-
const script = buildBootstrapScript(FIRST_BOOT_OPTS);
|
|
21
|
-
// Seeds that DO belong (config template + model ladders + pi settings +
|
|
22
|
-
// optional single auth.json).
|
|
23
|
-
assert.match(script, /settings\.json/);
|
|
24
|
-
assert.match(script, /pi-crtr-extensions/);
|
|
25
|
-
assert.match(script, /CRTR_CONFIG_JSON/);
|
|
26
|
-
assert.match(script, /PI_AUTH_JSON/);
|
|
27
|
-
// The rotation pool is dead product — bridge/migration only, never emitted
|
|
28
|
-
// by the rewritten bootstrap regardless of opts shape.
|
|
29
|
-
assert.doesNotMatch(script, /ANTHROPIC_SUBSCRIPTIONS_JSON/);
|
|
30
|
-
assert.doesNotMatch(script, /OPENAI_CODEX_SUBSCRIPTIONS_JSON/);
|
|
31
|
-
assert.doesNotMatch(script, /PROVIDER_ROTATION_JSON/);
|
|
32
|
-
assert.doesNotMatch(script, /anthropic-subscriptions\.json/);
|
|
33
|
-
assert.doesNotMatch(script, /openai-codex-subscriptions\.json/);
|
|
34
|
-
assert.doesNotMatch(script, /provider-rotation\.json/);
|
|
35
|
-
// No boot-time install of any kind — the image bakes crtr; bootstrap only
|
|
36
|
-
// asserts it.
|
|
37
|
-
assert.doesNotMatch(script, /npm install/);
|
|
38
|
-
});
|
|
39
|
-
test('recreate (isFirstProvision:false) emits no seeds at all and revives the existing node', () => {
|
|
40
|
-
const script = buildBootstrapScript({
|
|
41
|
-
...FIRST_BOOT_OPTS,
|
|
42
|
-
isFirstProvision: false,
|
|
43
|
-
nodeId: 'node-123',
|
|
44
|
-
});
|
|
45
|
-
assert.doesNotMatch(script, /settings\.json/);
|
|
46
|
-
assert.doesNotMatch(script, /CRTR_CONFIG_JSON/);
|
|
47
|
-
assert.doesNotMatch(script, /PI_AUTH_JSON/);
|
|
48
|
-
assert.doesNotMatch(script, /ANTHROPIC_SUBSCRIPTIONS_JSON/);
|
|
49
|
-
assert.doesNotMatch(script, /OPENAI_CODEX_SUBSCRIPTIONS_JSON/);
|
|
50
|
-
assert.doesNotMatch(script, /PROVIDER_ROTATION_JSON/);
|
|
51
|
-
assert.match(script, /'\/usr\/local\/bin\/crtr' canvas revive 'node-123'/);
|
|
52
|
-
assert.doesNotMatch(script, /node new --root/);
|
|
53
|
-
});
|
|
54
|
-
test('recreate without a nodeId fails fast at script-generation time, not inside the guest', () => {
|
|
55
|
-
assert.throws(() => buildBootstrapScript({ ...FIRST_BOOT_OPTS, isFirstProvision: false, nodeId: null }));
|
|
56
|
-
assert.throws(() => buildBootstrapScript({ ...FIRST_BOOT_OPTS, isFirstProvision: false, nodeId: ' ' }));
|
|
57
|
-
});
|
|
58
|
-
test('an empty relay token fails fast at script-generation time — the web lane must never bind unauthenticated', () => {
|
|
59
|
-
assert.throws(() => buildBootstrapScript({ ...FIRST_BOOT_OPTS, relayToken: '' }));
|
|
60
|
-
assert.throws(() => buildBootstrapScript({ ...FIRST_BOOT_OPTS, relayToken: ' ' }));
|
|
61
|
-
});
|
|
62
|
-
test('asserts the baked crtr command surface — never falls back to the retired top-level alias', () => {
|
|
63
|
-
const script = buildBootstrapScript(FIRST_BOOT_OPTS);
|
|
64
|
-
assert.match(script, /'\/usr\/local\/bin\/crtr' surface web -h/);
|
|
65
|
-
assert.match(script, /'\/usr\/local\/bin\/crtr' surface web serve -h/);
|
|
66
|
-
assert.match(script, /command surface assertion FAILED/);
|
|
67
|
-
assert.match(script, /'\/usr\/local\/bin\/crtr' surface web serve --host 0\.0\.0\.0/);
|
|
68
|
-
// Never the retired top-level alias, anywhere in the script (e.g. as a
|
|
69
|
-
// fallback if the surface assertion fails).
|
|
70
|
-
assert.doesNotMatch(script, /[^/]crtr web serve/);
|
|
71
|
-
});
|
|
72
|
-
test('asserts the pinned crtr version against the baked binary and fails closed on mismatch', () => {
|
|
73
|
-
const script = buildBootstrapScript(FIRST_BOOT_OPTS);
|
|
74
|
-
assert.match(script, /'\/usr\/local\/bin\/crtr' --json sys version/);
|
|
75
|
-
assert.match(script, /crtr version assertion FAILED/);
|
|
76
|
-
assert.match(script, /!= '0\.3\.33'/);
|
|
77
|
-
});
|
|
78
|
-
test('every web-serve invocation carries a non-empty --token, enforcing relay-token auth on every lane', () => {
|
|
79
|
-
const firstBoot = buildBootstrapScript(FIRST_BOOT_OPTS);
|
|
80
|
-
const recreate = buildBootstrapScript({ ...FIRST_BOOT_OPTS, isFirstProvision: false, nodeId: 'node-123' });
|
|
81
|
-
for (const script of [firstBoot, recreate]) {
|
|
82
|
-
const serveLine = script.split('\n').find((line) => line.includes('surface web serve --host'));
|
|
83
|
-
assert.ok(serveLine, 'expected a crtr surface web serve invocation');
|
|
84
|
-
assert.match(serveLine, /--token 'relay-token'/);
|
|
85
|
-
}
|
|
86
|
-
});
|
|
87
|
-
test('every crtr invocation targets the baked binary by absolute path, never a bare PATH lookup, defeating a stale volume-installed crtr shadowing it (review Major 1)', () => {
|
|
88
|
-
const firstBoot = buildBootstrapScript(FIRST_BOOT_OPTS);
|
|
89
|
-
const recreate = buildBootstrapScript({ ...FIRST_BOOT_OPTS, isFirstProvision: false, nodeId: 'node-123' });
|
|
90
|
-
for (const script of [firstBoot, recreate]) {
|
|
91
|
-
assert.match(script, /'\/usr\/local\/bin\/crtr' --json sys version/);
|
|
92
|
-
assert.match(script, /'\/usr\/local\/bin\/crtr' surface web -h/);
|
|
93
|
-
assert.match(script, /'\/usr\/local\/bin\/crtr' surface web serve -h/);
|
|
94
|
-
assert.match(script, /'\/usr\/local\/bin\/crtr' sys daemon start/);
|
|
95
|
-
assert.match(script, /'\/usr\/local\/bin\/crtr' surface web serve --host 0\.0\.0\.0/);
|
|
96
|
-
// No bare `crtr <subcommand>` invocation anywhere — only the `command -v
|
|
97
|
-
// crtr` shadow-detection probe and human-readable error-message text are
|
|
98
|
-
// allowed to reference it by bare name.
|
|
99
|
-
const bareInvocations = script
|
|
100
|
-
.split('\n')
|
|
101
|
-
.filter((line) => !line.trim().startsWith('printf'))
|
|
102
|
-
.filter((line) => /(^|[\s(])crtr (canvas|surface|--json)/.test(line));
|
|
103
|
-
assert.deepEqual(bareInvocations, []);
|
|
104
|
-
}
|
|
105
|
-
assert.match(firstBoot, /'\/usr\/local\/bin\/crtr' --json node new --root/);
|
|
106
|
-
});
|
|
107
|
-
test('asserts a bare `crtr` PATH lookup resolves to the baked binary, catching a stale volume-installed crtr shadowing it', () => {
|
|
108
|
-
const script = buildBootstrapScript(FIRST_BOOT_OPTS);
|
|
109
|
-
assert.match(script, /resolved_crtr="\$\(command -v crtr 2>\/dev\/null \|\| true\)"/);
|
|
110
|
-
assert.match(script, /if \[ "\$resolved_crtr" != '\/usr\/local\/bin\/crtr' \]; then/);
|
|
111
|
-
assert.match(script, /stale crtr shadowing baked binary/);
|
|
112
|
-
});
|
|
113
|
-
test('exports a PATH with the baked /usr/local/bin ahead of the legacy (dead) per-home runtime bin dir', () => {
|
|
114
|
-
const script = buildBootstrapScript(FIRST_BOOT_OPTS);
|
|
115
|
-
assert.match(script, /export PATH='\/usr\/local\/bin:\/usr\/bin:\/bin'/);
|
|
116
|
-
assert.doesNotMatch(script, /crtr-runtime\/bin/);
|
|
117
|
-
});
|
|
118
|
-
test('mkdirs the tmpfs CRTR_SOCK_DIR/CRTR_PIDFILE dirs strictly before the daemon starts, and CRTR_CANVAS_DB stays unset', () => {
|
|
119
|
-
const script = buildBootstrapScript(FIRST_BOOT_OPTS);
|
|
120
|
-
assert.match(script, /export CRTR_SOCK_DIR='\/run\/crtr\/socks'/);
|
|
121
|
-
assert.match(script, /export CRTR_PIDFILE='\/run\/crtr\/crtrd\.pid'/);
|
|
122
|
-
assert.match(script, /export CRTR_HOME='\/home\/agent\/\.crouter\/canvas'/);
|
|
123
|
-
assert.doesNotMatch(script, /CRTR_CANVAS_DB=/);
|
|
124
|
-
assert.match(script, /unset CRTR_CANVAS_DB/);
|
|
125
|
-
const mkdirIdx = indexOf(script, '/run/crtr/socks');
|
|
126
|
-
const daemonStartIdx = indexOf(script, "'/usr/local/bin/crtr' sys daemon start");
|
|
127
|
-
assert.ok(mkdirIdx < daemonStartIdx, 'tmpfs mkdir must precede daemon start');
|
|
128
|
-
const mkdirLine = script.split('\n').find((line) => line.startsWith('mkdir -p') && line.includes('/run/crtr/socks'));
|
|
129
|
-
assert.ok(mkdirLine, 'expected a single mkdir line covering the tmpfs dirs');
|
|
130
|
-
});
|
|
131
|
-
test('primes the baked pi Claude OAuth adapter into the npm cache path pi resolves, existence-gated', () => {
|
|
132
|
-
const script = buildBootstrapScript(FIRST_BOOT_OPTS);
|
|
133
|
-
assert.match(script, /\/opt\/crouter\/node_modules\/pi-claude-oauth-adapter/);
|
|
134
|
-
assert.match(script, /\.pi\/agent\/npm\/node_modules\/pi-claude-oauth-adapter/);
|
|
135
|
-
assert.match(script, /cp -R/);
|
|
136
|
-
});
|
|
@@ -1,151 +0,0 @@
|
|
|
1
|
-
// model-auth-guest.test.ts — focused P3.3b in-guest model-auth lane regression
|
|
2
|
-
// test, added under Silas's 2026-06-30 category-3 Hearth/control-plane
|
|
3
|
-
// carve-out (CLAUDE.md testing policy) for the load-bearing invariants named
|
|
4
|
-
// in the P3.3b implementation review
|
|
5
|
-
// (.crouter/canvas/nodes/mr1exjvw-73c75466/context/p3p3b-in-guest-model-auth-review.md,
|
|
6
|
-
// "Minor — no ported unit test for the in-guest lane"):
|
|
7
|
-
//
|
|
8
|
-
// 1. Tokenless/invalid-token access to the model-auth lane fails closed with
|
|
9
|
-
// 401 (strict relay-token gate; no loopback bypass) — the design-review
|
|
10
|
-
// Major that the lane must use `requestTokenMatches`, never
|
|
11
|
-
// `requestAllowed()` (which would accept a tokenless loopback request).
|
|
12
|
-
// 2. `status` returns 200 with a correct unauthenticated/authenticated body
|
|
13
|
-
// when there is exactly one pi `auth.json` in the intended store — locks
|
|
14
|
-
// in the fix for the old M0 wake-proxy 502 bug class.
|
|
15
|
-
// 3. No response body ever includes access/refresh credential material.
|
|
16
|
-
//
|
|
17
|
-
// This drives the real seam end-to-end: `startWebServer` (src/clients/web/
|
|
18
|
-
// server.ts) mounts `handleHearthModelAuthRequest`
|
|
19
|
-
// (../model-auth-guest.ts) behind its relay-token gate, and the test points
|
|
20
|
-
// pi's real `AuthStorage` at a temp dir via `PI_CODING_AGENT_DIR` (the same
|
|
21
|
-
// env var `getAgentDir()` honors in production) so the lane reads/writes a
|
|
22
|
-
// real single `auth.json` with no mocking of the module under test.
|
|
23
|
-
import assert from 'node:assert/strict';
|
|
24
|
-
import { mkdtempSync, rmSync, writeFileSync } from 'node:fs';
|
|
25
|
-
import { tmpdir } from 'node:os';
|
|
26
|
-
import { join } from 'node:path';
|
|
27
|
-
import { test } from 'node:test';
|
|
28
|
-
import { AuthStorage } from '@earendil-works/pi-coding-agent';
|
|
29
|
-
import { startWebServer } from '../../../clients/web/server.js';
|
|
30
|
-
const RELAY_TOKEN = 'model-auth-test-relay-token';
|
|
31
|
-
const STATUS_PATH = '/__hearth/model-auth/anthropic/status';
|
|
32
|
-
const START_PATH = '/__hearth/model-auth/anthropic/start';
|
|
33
|
-
const FINISH_PATH = '/__hearth/model-auth/anthropic/finish';
|
|
34
|
-
async function withServer(fn) {
|
|
35
|
-
const server = await startWebServer({ port: 0, host: '127.0.0.1', token: RELAY_TOKEN });
|
|
36
|
-
try {
|
|
37
|
-
await fn(server);
|
|
38
|
-
}
|
|
39
|
-
finally {
|
|
40
|
-
await server.close();
|
|
41
|
-
}
|
|
42
|
-
}
|
|
43
|
-
/** Point pi's AuthStorage at a fresh temp dir (the same env var `getAgentDir()`
|
|
44
|
-
* reads in production) for the duration of `fn`, then restore the prior
|
|
45
|
-
* value. Returns the temp dir so a caller can seed `auth.json` before use. */
|
|
46
|
-
async function withTempAgentDir(fn) {
|
|
47
|
-
const agentDir = mkdtempSync(join(tmpdir(), 'model-auth-guest-test-'));
|
|
48
|
-
const prior = process.env['PI_CODING_AGENT_DIR'];
|
|
49
|
-
process.env['PI_CODING_AGENT_DIR'] = agentDir;
|
|
50
|
-
try {
|
|
51
|
-
await fn(agentDir);
|
|
52
|
-
}
|
|
53
|
-
finally {
|
|
54
|
-
if (prior === undefined) {
|
|
55
|
-
delete process.env['PI_CODING_AGENT_DIR'];
|
|
56
|
-
}
|
|
57
|
-
else {
|
|
58
|
-
process.env['PI_CODING_AGENT_DIR'] = prior;
|
|
59
|
-
}
|
|
60
|
-
rmSync(agentDir, { recursive: true, force: true });
|
|
61
|
-
}
|
|
62
|
-
}
|
|
63
|
-
test('model-auth lane fails closed with 401 for tokenless and invalid-token requests (no loopback bypass)', async () => {
|
|
64
|
-
await withTempAgentDir(async (agentDir) => {
|
|
65
|
-
// Exactly one auth.json present — proves the 401 is the token gate, not a
|
|
66
|
-
// downstream store-resolution failure.
|
|
67
|
-
writeFileSync(join(agentDir, 'auth.json'), '{}\n', 'utf8');
|
|
68
|
-
await withServer(async (server) => {
|
|
69
|
-
// No credentials at all, on loopback — this is the design-review Major:
|
|
70
|
-
// the lane must use the strict `requestTokenMatches` predicate, never
|
|
71
|
-
// `requestAllowed()`, which accepts a tokenless loopback request for every
|
|
72
|
-
// OTHER route on this same server.
|
|
73
|
-
for (const [method, path] of [
|
|
74
|
-
['GET', STATUS_PATH],
|
|
75
|
-
['POST', START_PATH],
|
|
76
|
-
['POST', FINISH_PATH],
|
|
77
|
-
]) {
|
|
78
|
-
const res = await fetch(`${server.url}${path}`, { method });
|
|
79
|
-
assert.equal(res.status, 401, `${method} ${path} with no token`);
|
|
80
|
-
}
|
|
81
|
-
// Wrong bearer token.
|
|
82
|
-
const wrongBearer = await fetch(`${server.url}${STATUS_PATH}`, {
|
|
83
|
-
headers: { authorization: 'Bearer not-the-relay-token' },
|
|
84
|
-
});
|
|
85
|
-
assert.equal(wrongBearer.status, 401);
|
|
86
|
-
// Wrong query token.
|
|
87
|
-
const wrongQuery = await fetch(`${server.url}${STATUS_PATH}?token=not-the-relay-token`);
|
|
88
|
-
assert.equal(wrongQuery.status, 401);
|
|
89
|
-
// Sanity: the SAME server, SAME loopback bind, with the correct token
|
|
90
|
-
// succeeds — proves the 401s above are the gate rejecting bad
|
|
91
|
-
// credentials, not some other failure making every request fail.
|
|
92
|
-
const correct = await fetch(`${server.url}${STATUS_PATH}`, {
|
|
93
|
-
headers: { authorization: `Bearer ${RELAY_TOKEN}` },
|
|
94
|
-
});
|
|
95
|
-
assert.equal(correct.status, 200);
|
|
96
|
-
});
|
|
97
|
-
});
|
|
98
|
-
});
|
|
99
|
-
test('model-auth status returns 200 with correct unauthenticated/authenticated state for a single auth.json (M0 502 regression)', async () => {
|
|
100
|
-
await withServer(async (server) => {
|
|
101
|
-
// Unauthenticated: exactly one auth.json, no anthropic credential stored.
|
|
102
|
-
await withTempAgentDir(async (agentDir) => {
|
|
103
|
-
writeFileSync(join(agentDir, 'auth.json'), '{}\n', 'utf8');
|
|
104
|
-
const res = await fetch(`${server.url}${STATUS_PATH}`, {
|
|
105
|
-
headers: { authorization: `Bearer ${RELAY_TOKEN}` },
|
|
106
|
-
});
|
|
107
|
-
assert.equal(res.status, 200);
|
|
108
|
-
const body = (await res.json());
|
|
109
|
-
assert.deepEqual(body, { storedOauth: false, expiresAt: null, adapterConfigured: true });
|
|
110
|
-
});
|
|
111
|
-
// Authenticated: exactly one auth.json, with a real stored anthropic oauth
|
|
112
|
-
// credential written through the real AuthStorage (not hand-crafted JSON).
|
|
113
|
-
await withTempAgentDir(async () => {
|
|
114
|
-
const storage = AuthStorage.create();
|
|
115
|
-
storage.set('anthropic', { type: 'oauth', access: 'access-secret', refresh: 'refresh-secret', expires: 4_102_444_800_000 });
|
|
116
|
-
assert.deepEqual(storage.drainErrors(), []);
|
|
117
|
-
const res = await fetch(`${server.url}${STATUS_PATH}`, {
|
|
118
|
-
headers: { authorization: `Bearer ${RELAY_TOKEN}` },
|
|
119
|
-
});
|
|
120
|
-
assert.equal(res.status, 200);
|
|
121
|
-
const body = (await res.json());
|
|
122
|
-
assert.deepEqual(body, { storedOauth: true, expiresAt: 4_102_444_800_000, adapterConfigured: true });
|
|
123
|
-
});
|
|
124
|
-
});
|
|
125
|
-
});
|
|
126
|
-
test('model-auth status response never includes access/refresh credential material', async () => {
|
|
127
|
-
await withServer(async (server) => {
|
|
128
|
-
await withTempAgentDir(async () => {
|
|
129
|
-
const storage = AuthStorage.create();
|
|
130
|
-
storage.set('anthropic', {
|
|
131
|
-
type: 'oauth',
|
|
132
|
-
access: 'super-secret-access-token',
|
|
133
|
-
refresh: 'super-secret-refresh-token',
|
|
134
|
-
expires: 4_102_444_800_000,
|
|
135
|
-
});
|
|
136
|
-
assert.deepEqual(storage.drainErrors(), []);
|
|
137
|
-
const res = await fetch(`${server.url}${STATUS_PATH}`, {
|
|
138
|
-
headers: { authorization: `Bearer ${RELAY_TOKEN}` },
|
|
139
|
-
});
|
|
140
|
-
assert.equal(res.status, 200);
|
|
141
|
-
const rawText = await res.text();
|
|
142
|
-
const body = JSON.parse(rawText);
|
|
143
|
-
// The response is exactly the redacted status shape — no access/refresh
|
|
144
|
-
// keys or raw secret material anywhere in the body.
|
|
145
|
-
assert.deepEqual(Object.keys(body).sort(), ['adapterConfigured', 'expiresAt', 'storedOauth']);
|
|
146
|
-
assert.equal(rawText.includes('super-secret-access-token'), false);
|
|
147
|
-
assert.equal(rawText.includes('super-secret-refresh-token'), false);
|
|
148
|
-
assert.equal(/access|refresh/i.test(rawText), false);
|
|
149
|
-
});
|
|
150
|
-
});
|
|
151
|
-
});
|
|
@@ -1,108 +0,0 @@
|
|
|
1
|
-
import { readFileSync } from 'node:fs';
|
|
2
|
-
import { homedir } from 'node:os';
|
|
3
|
-
import { join, resolve } from 'node:path';
|
|
4
|
-
import { general } from '../errors.js';
|
|
5
|
-
const ENV = {
|
|
6
|
-
provider: 'CRTR_HEARTH_PROVIDER',
|
|
7
|
-
guestUser: 'CRTR_HEARTH_M0_GUEST_USER',
|
|
8
|
-
guestHome: 'CRTR_HEARTH_M0_GUEST_HOME',
|
|
9
|
-
guestCode: 'CRTR_HEARTH_M0_GUEST_CODE',
|
|
10
|
-
webPort: 'CRTR_HEARTH_M0_WEB_PORT',
|
|
11
|
-
image: 'CRTR_HEARTH_M0_IMAGE',
|
|
12
|
-
templateVersion: 'CRTR_HEARTH_M0_TEMPLATE_VERSION',
|
|
13
|
-
volumeSizeGb: 'CRTR_HEARTH_M0_VOLUME_SIZE_GB',
|
|
14
|
-
nodeKind: 'CRTR_HEARTH_M0_NODE_KIND',
|
|
15
|
-
piPackages: 'CRTR_HEARTH_M0_PI_PACKAGES',
|
|
16
|
-
piAuthJson: 'CRTR_HEARTH_M0_PI_AUTH_JSON',
|
|
17
|
-
relayToken: 'CRTR_HEARTH_M0_RELAY_TOKEN',
|
|
18
|
-
};
|
|
19
|
-
function expandHomePath(input) {
|
|
20
|
-
if (input.startsWith('~'))
|
|
21
|
-
return join(homedir(), input.slice(1));
|
|
22
|
-
return resolve(input);
|
|
23
|
-
}
|
|
24
|
-
function parsePort(value, fallback, name) {
|
|
25
|
-
if (value === undefined || value === '')
|
|
26
|
-
return fallback;
|
|
27
|
-
const parsed = Number(value);
|
|
28
|
-
if (!Number.isInteger(parsed) || parsed <= 0 || parsed > 65535) {
|
|
29
|
-
throw general(`invalid ${name}: ${value}`, { env: name });
|
|
30
|
-
}
|
|
31
|
-
return parsed;
|
|
32
|
-
}
|
|
33
|
-
function parseVolumeSizeGb(value, fallback, name) {
|
|
34
|
-
if (value === undefined || value === '')
|
|
35
|
-
return fallback;
|
|
36
|
-
const parsed = Number(value);
|
|
37
|
-
if (!Number.isInteger(parsed) || parsed < 4) {
|
|
38
|
-
throw general(`invalid ${name}: ${value}`, { env: name, minimumGb: 4 });
|
|
39
|
-
}
|
|
40
|
-
return parsed;
|
|
41
|
-
}
|
|
42
|
-
function readCrtrVersion() {
|
|
43
|
-
const file = new URL('../../../package.json', import.meta.url);
|
|
44
|
-
const pkg = JSON.parse(readFileSync(file, 'utf8'));
|
|
45
|
-
if (typeof pkg.version !== 'string' || pkg.version.trim() === '') {
|
|
46
|
-
throw general('missing crtr package version', { packageJson: file.pathname });
|
|
47
|
-
}
|
|
48
|
-
return pkg.version;
|
|
49
|
-
}
|
|
50
|
-
export function hearthHomeImageRefForVersion(version) {
|
|
51
|
-
return `crouter-hearth-home-${version.replace(/\./g, '-')}`;
|
|
52
|
-
}
|
|
53
|
-
const SAFE_PI_PACKAGE_SPEC = /^[A-Za-z0-9@/_:.-]+$/;
|
|
54
|
-
const DEFAULT_PI_PACKAGES = ['npm:pi-claude-oauth-adapter@0.1.2'];
|
|
55
|
-
function parsePiPackages(env) {
|
|
56
|
-
const raw = env[ENV.piPackages]?.trim();
|
|
57
|
-
if (raw === undefined || raw === '')
|
|
58
|
-
return [...DEFAULT_PI_PACKAGES];
|
|
59
|
-
const tokens = raw.split(/[,\s]+/).map((token) => token.trim()).filter((token) => token !== '');
|
|
60
|
-
if (tokens.length === 0) {
|
|
61
|
-
throw general(`invalid ${ENV.piPackages}: ${raw}`, { env: ENV.piPackages });
|
|
62
|
-
}
|
|
63
|
-
if (tokens.length === 1 && tokens[0]?.toLowerCase() === 'none')
|
|
64
|
-
return [];
|
|
65
|
-
if (tokens.some((token) => token.toLowerCase() === 'none')) {
|
|
66
|
-
throw general(`invalid ${ENV.piPackages}: none must be the only value`, { env: ENV.piPackages });
|
|
67
|
-
}
|
|
68
|
-
for (const spec of tokens) {
|
|
69
|
-
if (!SAFE_PI_PACKAGE_SPEC.test(spec)) {
|
|
70
|
-
throw general(`invalid ${ENV.piPackages}: ${spec}`, { env: ENV.piPackages, packageSpec: spec });
|
|
71
|
-
}
|
|
72
|
-
}
|
|
73
|
-
return tokens;
|
|
74
|
-
}
|
|
75
|
-
export function loadM0Config(env = process.env) {
|
|
76
|
-
const providerName = (env[ENV.provider] ?? '').trim().toLowerCase();
|
|
77
|
-
const guestUser = (env[ENV.guestUser] ?? 'agent').trim();
|
|
78
|
-
const guestHome = expandHomePath(env[ENV.guestHome] ?? '/home/agent');
|
|
79
|
-
const guestCodePath = expandHomePath(env[ENV.guestCode] ?? '/opt/crouter');
|
|
80
|
-
const piPackages = parsePiPackages(env);
|
|
81
|
-
const webPort = parsePort(env[ENV.webPort], 7878, ENV.webPort);
|
|
82
|
-
const templateVersion = (env[ENV.templateVersion] ?? readCrtrVersion()).trim();
|
|
83
|
-
if (templateVersion === '') {
|
|
84
|
-
throw general(`invalid ${ENV.templateVersion}: empty`, { env: ENV.templateVersion });
|
|
85
|
-
}
|
|
86
|
-
const imageRef = (env[ENV.image] ?? hearthHomeImageRefForVersion(templateVersion)).trim();
|
|
87
|
-
if (imageRef === '') {
|
|
88
|
-
throw general(`invalid ${ENV.image}: empty`, { env: ENV.image });
|
|
89
|
-
}
|
|
90
|
-
const volumeSizeGb = parseVolumeSizeGb(env[ENV.volumeSizeGb], 4, ENV.volumeSizeGb);
|
|
91
|
-
const nodeKind = (env[ENV.nodeKind] ?? 'general').trim();
|
|
92
|
-
const piAuthJson = env[ENV.piAuthJson]?.trim() || null;
|
|
93
|
-
const relayToken = env[ENV.relayToken]?.trim() || null;
|
|
94
|
-
return {
|
|
95
|
-
providerName,
|
|
96
|
-
guestUser,
|
|
97
|
-
guestHome,
|
|
98
|
-
guestCodePath,
|
|
99
|
-
piPackages,
|
|
100
|
-
webPort,
|
|
101
|
-
imageRef,
|
|
102
|
-
templateVersion,
|
|
103
|
-
volumeSizeGb,
|
|
104
|
-
nodeKind,
|
|
105
|
-
piAuthJson,
|
|
106
|
-
relayToken,
|
|
107
|
-
};
|
|
108
|
-
}
|
|
@@ -1,9 +0,0 @@
|
|
|
1
|
-
import type { M0Config } from './types.js';
|
|
2
|
-
/** The complete home env table per R-DUR-8 — the one place that owns it.
|
|
3
|
-
* `HOME`/`CRTR_HOME` resolve onto the durable user volume (canvas/auth/config
|
|
4
|
-
* state survives destroy→recreate there); `CRTR_SOCK_DIR`/`CRTR_PIDFILE`
|
|
5
|
-
* resolve onto tmpfs (rebound fresh every boot, never stale-on-volume);
|
|
6
|
-
* `CRTR_CANVAS_DB` stays UNSET so `canvas.db` follows `CRTR_HOME` onto the
|
|
7
|
-
* volume (its fcntl/WAL locks need real local-disk semantics, which the
|
|
8
|
-
* block volume provides — unlike the NFS case `CRTR_CANVAS_DB` exists for). */
|
|
9
|
-
export declare function guestEnv(config: M0Config): Record<string, string>;
|
|
@@ -1,27 +0,0 @@
|
|
|
1
|
-
import { join } from 'node:path';
|
|
2
|
-
// tmpfs base for the home's socket + pidfile (R-DUR-8/R-HOME-3): a unix
|
|
3
|
-
// socket cannot bind on the durable NFS/block volume and a pidfile must never
|
|
4
|
-
// be stale-on-volume across destroy/recreate, so both live process-local on
|
|
5
|
-
// tmpfs instead. `/run` is the conventional tmpfs mount on the baked Blaxel
|
|
6
|
-
// image; the bootstrap chain (blaxel-bootstrap.ts) mkdirs this before the
|
|
7
|
-
// daemon starts.
|
|
8
|
-
const GUEST_TMPFS_BASE = '/run/crtr';
|
|
9
|
-
/** The complete home env table per R-DUR-8 — the one place that owns it.
|
|
10
|
-
* `HOME`/`CRTR_HOME` resolve onto the durable user volume (canvas/auth/config
|
|
11
|
-
* state survives destroy→recreate there); `CRTR_SOCK_DIR`/`CRTR_PIDFILE`
|
|
12
|
-
* resolve onto tmpfs (rebound fresh every boot, never stale-on-volume);
|
|
13
|
-
* `CRTR_CANVAS_DB` stays UNSET so `canvas.db` follows `CRTR_HOME` onto the
|
|
14
|
-
* volume (its fcntl/WAL locks need real local-disk semantics, which the
|
|
15
|
-
* block volume provides — unlike the NFS case `CRTR_CANVAS_DB` exists for). */
|
|
16
|
-
export function guestEnv(config) {
|
|
17
|
-
const home = config.guestHome;
|
|
18
|
-
return {
|
|
19
|
-
HOME: home,
|
|
20
|
-
CRTR_HOME: join(home, '.crouter', 'canvas'),
|
|
21
|
-
CRTR_SOCK_DIR: join(GUEST_TMPFS_BASE, 'socks'),
|
|
22
|
-
CRTR_PIDFILE: join(GUEST_TMPFS_BASE, 'crtrd.pid'),
|
|
23
|
-
PI_CODING_AGENT_DIR: join(home, '.pi', 'agent'),
|
|
24
|
-
PATH: `${join(home, '.crtr-runtime', 'bin')}:/usr/local/bin:/usr/bin:/bin`,
|
|
25
|
-
SANDBOX_DISABLE_PROCESS_LOGGING: 'true',
|
|
26
|
-
};
|
|
27
|
-
}
|
|
@@ -1,8 +0,0 @@
|
|
|
1
|
-
import type { IncomingMessage, ServerResponse } from 'node:http';
|
|
2
|
-
/** Dispatch one `/__hearth/model-auth/anthropic/{status,start,finish}` request.
|
|
3
|
-
* The caller (src/clients/web/server.ts) has already matched the path prefix
|
|
4
|
-
* and enforced the relay token — this function assumes both are done and
|
|
5
|
-
* handles ONLY routing-within-the-lane, method checks, and the Hearth/OAuth
|
|
6
|
-
* product logic. Always writes a response; never throws (errors become a
|
|
7
|
-
* redacted 502/500 JSON body). */
|
|
8
|
-
export declare function handleHearthModelAuthRequest(req: IncomingMessage, res: ServerResponse): Promise<void>;
|