switchroom 0.7.15 → 0.10.0
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/README.md +51 -59
- package/bin/run-hook.sh +27 -11
- package/bin/timezone-hook.sh +9 -7
- package/dist/agent-scheduler/index.js +410 -133
- package/dist/auth-broker/index.js +13932 -0
- package/dist/cli/switchroom.js +26937 -5601
- package/dist/host-control/main.js +12702 -0
- package/dist/vault/approvals/kernel-server.js +467 -184
- package/dist/vault/broker/server.js +1430 -724
- package/examples/minimal.yaml +63 -0
- package/examples/personal-google-workspace-mcp/.env.example +34 -0
- package/examples/personal-google-workspace-mcp/README.md +194 -0
- package/examples/personal-google-workspace-mcp/compose.yaml +66 -0
- package/examples/switchroom.yaml +220 -0
- package/package.json +7 -4
- package/profiles/_base/settings.json.hbs +20 -5
- package/profiles/_base/start.sh.hbs +16 -3
- package/profiles/_shared/agent-self-service.md.hbs +126 -0
- package/profiles/_shared/telegram-style.md.hbs +20 -90
- package/profiles/_shared/vault-protocol.md.hbs +68 -0
- package/profiles/default/CLAUDE.md +50 -96
- package/profiles/default/CLAUDE.md.hbs +36 -6
- package/profiles/default/workspace/SOUL.md.hbs +12 -5
- package/skills/buildkite-agent-infrastructure/SKILL.md +30 -11
- package/skills/buildkite-agent-runtime/SKILL.md +44 -11
- package/skills/buildkite-api/SKILL.md +31 -8
- package/skills/buildkite-cli/SKILL.md +27 -9
- package/skills/buildkite-migration/SKILL.md +22 -9
- package/skills/buildkite-pipelines/SKILL.md +26 -9
- package/skills/buildkite-secure-delivery/SKILL.md +23 -9
- package/skills/buildkite-test-engine/SKILL.md +25 -8
- package/skills/docx/SKILL.md +1 -1
- package/skills/docx/scripts/office/validators/__pycache__/__init__.cpython-313.pyc +0 -0
- package/skills/docx/scripts/office/validators/__pycache__/base.cpython-313.pyc +0 -0
- package/skills/file-bug/SKILL.md +34 -6
- package/skills/humanizer/SKILL.md +15 -0
- package/skills/humanizer-calibrate/SKILL.md +7 -1
- package/skills/mcp-builder/SKILL.md +1 -1
- package/skills/pdf/SKILL.md +1 -1
- package/skills/pptx/SKILL.md +1 -1
- package/skills/skill-creator/SKILL.md +21 -1
- package/skills/skill-creator/scripts/__pycache__/__init__.cpython-313.pyc +0 -0
- package/skills/skill-creator/scripts/__pycache__/generate_report.cpython-313.pyc +0 -0
- package/skills/skill-creator/scripts/__pycache__/improve_description.cpython-313.pyc +0 -0
- package/skills/skill-creator/scripts/__pycache__/run_eval.cpython-313.pyc +0 -0
- package/skills/skill-creator/scripts/__pycache__/run_loop.cpython-313.pyc +0 -0
- package/skills/skill-creator/scripts/__pycache__/utils.cpython-313.pyc +0 -0
- package/skills/switchroom-cli/SKILL.md +63 -64
- package/skills/switchroom-health/SKILL.md +23 -10
- package/skills/switchroom-install/SKILL.md +3 -3
- package/skills/switchroom-manage/SKILL.md +26 -19
- package/skills/switchroom-runtime/SKILL.md +191 -0
- package/skills/switchroom-status/SKILL.md +27 -2
- package/skills/telegram-test-harness/SKILL.md +3 -0
- package/skills/token-helpers/SKILL.md +24 -1
- package/skills/webapp-testing/SKILL.md +31 -1
- package/skills/xlsx/SKILL.md +1 -1
- package/telegram-plugin/admin-commands/index.ts +7 -5
- package/telegram-plugin/analytics-posthog.ts +191 -0
- package/telegram-plugin/bridge/bridge.ts +69 -0
- package/telegram-plugin/bridge/ipc-client.ts +4 -1
- package/telegram-plugin/dist/bridge/bridge.js +194 -119
- package/telegram-plugin/dist/gateway/gateway.js +23611 -19671
- package/telegram-plugin/dist/server.js +245 -189
- package/telegram-plugin/first-paint.ts +3 -24
- package/telegram-plugin/gateway/auth-add-flow.ts +326 -0
- package/telegram-plugin/gateway/auth-broker-client.ts +75 -0
- package/telegram-plugin/gateway/auth-command.ts +794 -0
- package/telegram-plugin/gateway/auth-line.ts +123 -0
- package/telegram-plugin/gateway/boot-card.ts +169 -40
- package/telegram-plugin/gateway/boot-issue-cache.ts +308 -0
- package/telegram-plugin/gateway/boot-probes.ts +166 -123
- package/telegram-plugin/gateway/boot-reason.ts +41 -7
- package/telegram-plugin/gateway/boot-version.ts +66 -0
- package/telegram-plugin/gateway/gateway.ts +3499 -1885
- package/telegram-plugin/gateway/hostd-dispatch.ts +117 -0
- package/telegram-plugin/gateway/ipc-protocol.ts +18 -0
- package/telegram-plugin/gateway/pending-inbound-buffer.ts +106 -0
- package/telegram-plugin/gateway/quarantine.ts +69 -0
- package/telegram-plugin/gateway/quota-cache.ts +9 -4
- package/telegram-plugin/gateway/reaction-trigger.ts +401 -0
- package/telegram-plugin/gateway/recent-denials.test.ts +103 -0
- package/telegram-plugin/gateway/recent-denials.ts +77 -0
- package/telegram-plugin/gateway/startup-network-retry.ts +109 -31
- package/telegram-plugin/gateway/vault-grant-inbound-builders.ts +125 -0
- package/telegram-plugin/history.ts +91 -0
- package/telegram-plugin/hooks/hooks.json +10 -0
- package/telegram-plugin/hooks/sandbox-hint-posttool.mjs +130 -0
- package/telegram-plugin/hooks/subagent-tracker-posttool.mjs +19 -2
- package/telegram-plugin/hooks/subagent-tracker-pretool.mjs +22 -2
- package/telegram-plugin/hooks/tool-label-pretool.mjs +11 -0
- package/telegram-plugin/hooks/wedge-detect-posttool.mjs +303 -0
- package/telegram-plugin/inbound-classifier.ts +50 -0
- package/telegram-plugin/inline-keyboard-callbacks.ts +136 -0
- package/telegram-plugin/node_modules/.vite/vitest/da39a3ee5e6b4b0d3255bfef95601890afd80709/results.json +1 -0
- package/telegram-plugin/package.json +4 -2
- package/telegram-plugin/permission-rule.ts +51 -0
- package/telegram-plugin/permission-title.ts +56 -0
- package/telegram-plugin/quota-check.ts +19 -41
- package/telegram-plugin/registry/reaper.ts +223 -0
- package/telegram-plugin/retry-api-call.ts +80 -0
- package/telegram-plugin/runtime-metrics.ts +177 -0
- package/telegram-plugin/scripts/build.mjs +0 -1
- package/telegram-plugin/secret-detect/index.ts +24 -0
- package/telegram-plugin/secret-detect/vault-error.test.ts +64 -12
- package/telegram-plugin/secret-detect/vault-error.ts +78 -11
- package/telegram-plugin/secret-detect/vault-write.ts +14 -2
- package/telegram-plugin/server.js +41795 -0
- package/telegram-plugin/session-tail.ts +6 -1
- package/telegram-plugin/shared/bot-runtime.ts +5 -4
- package/telegram-plugin/silence-poke.ts +420 -0
- package/telegram-plugin/silent-end.ts +174 -0
- package/telegram-plugin/stream-controller.ts +13 -0
- package/telegram-plugin/stream-reply-handler.ts +7 -0
- package/telegram-plugin/subagent-watcher.ts +213 -4
- package/telegram-plugin/tests/auth-add-flow.test.ts +559 -0
- package/telegram-plugin/tests/auth-code-redact.test.ts +8 -4
- package/telegram-plugin/tests/auth-command-vernacular.test.ts +531 -0
- package/telegram-plugin/tests/boot-card-issue-dedup.test.ts +247 -0
- package/telegram-plugin/tests/boot-card-reason-to-render.test.ts +182 -0
- package/telegram-plugin/tests/boot-card-reason.test.ts +65 -2
- package/telegram-plugin/tests/boot-card-render.test.ts +146 -0
- package/telegram-plugin/tests/boot-card-silent-on-operator.test.ts +103 -0
- package/telegram-plugin/tests/boot-probes.test.ts +216 -10
- package/telegram-plugin/tests/boot-version-string.test.ts +0 -0
- package/telegram-plugin/tests/finalize-callback.test.ts +190 -0
- package/telegram-plugin/tests/gateway-message-validator.test.ts +26 -0
- package/telegram-plugin/tests/gateway-secret-detect.test.ts +12 -3
- package/telegram-plugin/tests/gateway-startup-network-retry.test.ts +104 -0
- package/telegram-plugin/tests/history-reaper.test.ts +378 -0
- package/telegram-plugin/tests/hostd-dispatch.test.ts +129 -0
- package/telegram-plugin/tests/inbound-classifier.test.ts +76 -0
- package/telegram-plugin/tests/inbound-message-types.test.ts +267 -0
- package/telegram-plugin/tests/issues-card.test.ts +49 -0
- package/telegram-plugin/tests/pending-inbound-buffer.test.ts +132 -0
- package/telegram-plugin/tests/permission-rule.test.ts +80 -1
- package/telegram-plugin/tests/permission-title.test.ts +31 -0
- package/telegram-plugin/tests/quota-check.test.ts +5 -35
- package/telegram-plugin/tests/races.test.ts +179 -0
- package/telegram-plugin/tests/reaction-trigger-flow.test.ts +353 -0
- package/telegram-plugin/tests/reaction-trigger.test.ts +397 -0
- package/telegram-plugin/tests/retry-api-call.test.ts +152 -1
- package/telegram-plugin/tests/runtime-metrics.test.ts +145 -0
- package/telegram-plugin/tests/sandbox-hint-posttool.test.ts +155 -0
- package/telegram-plugin/tests/secret-detect-delete-must-surface-failures.test.ts +133 -0
- package/telegram-plugin/tests/secret-detect-false-positives.test.ts +137 -0
- package/telegram-plugin/tests/silence-poke.test.ts +493 -0
- package/telegram-plugin/tests/silent-end.test.ts +206 -0
- package/telegram-plugin/tests/subagent-tracker-hooks.test.ts +107 -0
- package/telegram-plugin/tests/subagent-watcher-env-thresholds.test.ts +224 -0
- package/telegram-plugin/tests/subagent-watcher-stall-terminal.test.ts +316 -0
- package/telegram-plugin/tests/subagent-watcher.test.ts +263 -0
- package/telegram-plugin/tests/turn-signal-tracker.test.ts +81 -0
- package/telegram-plugin/tests/vault-approval-posture.test.ts +256 -0
- package/telegram-plugin/tests/vault-grant-auto-resume.test.ts +73 -0
- package/telegram-plugin/tests/vault-grant-inbound-builders.test.ts +226 -0
- package/telegram-plugin/tests/vault-grant-union.test.ts +130 -0
- package/telegram-plugin/tests/vault-key-regex-allows-slash.test.ts +140 -0
- package/telegram-plugin/tests/vault-posture-quarantine.test.ts +104 -0
- package/telegram-plugin/tests/vault-request-access-tool.test.ts +114 -0
- package/telegram-plugin/tests/vault-request-access-unlock-resume.test.ts +106 -0
- package/telegram-plugin/turn-signal-tracker.ts +100 -24
- package/telegram-plugin/uat/SETUP.md +210 -35
- package/telegram-plugin/uat/assertions.ts +264 -37
- package/telegram-plugin/uat/driver-info.ts +57 -0
- package/telegram-plugin/uat/driver.ts +590 -51
- package/telegram-plugin/uat/harness.ts +140 -94
- package/telegram-plugin/uat/load-env.test.ts +72 -0
- package/telegram-plugin/uat/load-env.ts +48 -0
- package/telegram-plugin/uat/login.ts +96 -53
- package/telegram-plugin/uat/runners/agent-self-sufficiency.ts +457 -0
- package/telegram-plugin/uat/runners/paraphrases.ts +231 -0
- package/telegram-plugin/uat/runners/report.ts +150 -0
- package/telegram-plugin/uat/runners/run-agent-self-sufficiency.sh +50 -0
- package/telegram-plugin/uat/runners/scorer.test.ts +196 -0
- package/telegram-plugin/uat/runners/scorer.ts +106 -0
- package/telegram-plugin/uat/runners/skill-coverage.test.ts +100 -0
- package/telegram-plugin/uat/runners/skill-coverage.ts +620 -0
- package/telegram-plugin/uat/scenarios/ask-user-button-tap-dm.test.ts +141 -0
- package/telegram-plugin/uat/scenarios/bg-sub-agent-dispatch-dm.test.ts +191 -0
- package/telegram-plugin/uat/scenarios/fuzz-extended-dm.test.ts +255 -0
- package/telegram-plugin/uat/scenarios/fuzz-human-style-dm.test.ts +275 -0
- package/telegram-plugin/uat/scenarios/fuzz-random-prompts-dm.test.ts +146 -0
- package/telegram-plugin/uat/scenarios/fuzz-status-ask-dm.test.ts +486 -0
- package/telegram-plugin/uat/scenarios/jtbd-interrupt-marker-dm.test.ts +67 -0
- package/telegram-plugin/uat/scenarios/jtbd-rapid-followup-dm.test.ts +100 -0
- package/telegram-plugin/uat/scenarios/jtbd-soft-commit-dm.test.ts +67 -0
- package/telegram-plugin/uat/scenarios/jtbd-status-query-dm.test.ts +49 -0
- package/telegram-plugin/uat/scenarios/location-inbound-dm.test.ts +65 -0
- package/telegram-plugin/uat/scenarios/midturn-silent-dm.test.ts +175 -0
- package/telegram-plugin/uat/scenarios/reactions-dm.test.ts +142 -0
- package/telegram-plugin/uat/scenarios/reactions-trigger-turn-dm.test.ts +96 -0
- package/telegram-plugin/uat/scenarios/secret-redaction-deletes-original-dm.test.ts +123 -0
- package/telegram-plugin/uat/scenarios/secret-redaction-no-false-positive-dm.test.ts +87 -0
- package/telegram-plugin/uat/scenarios/silence-poke-soft-dm.test.ts +155 -0
- package/telegram-plugin/uat/scenarios/silent-end-recovery-dm.test.ts +95 -0
- package/telegram-plugin/uat/scenarios/smoke-dm-reply.test.ts +57 -0
- package/telegram-plugin/uat/scenarios/subagent-watcher-no-rerun-dm.test.ts +135 -0
- package/telegram-plugin/uat/scenarios/vault-approval-posture-telegram-id-dm.test.ts +191 -0
- package/telegram-plugin/uat/scenarios/vault-audit-allow-dm.test.ts +108 -0
- package/telegram-plugin/uat/scenarios/vault-grant-auto-resume-dm.test.ts +121 -0
- package/telegram-plugin/uat/scenarios/vault-request-access-concurrent-dm.test.ts +161 -0
- package/telegram-plugin/uat/scenarios/vault-request-access-end-to-end-dm.test.ts +158 -0
- package/telegram-plugin/uat/scenarios/voice-inbound-dm.test.ts +65 -0
- package/telegram-plugin/vault-approval-posture.ts +42 -0
- package/telegram-plugin/welcome-text.ts +1 -0
- package/telegram-plugin/active-pins-sweep.ts +0 -204
- package/telegram-plugin/active-pins.ts +0 -146
- package/telegram-plugin/auth-dashboard.ts +0 -1104
- package/telegram-plugin/auth-slot-parser.ts +0 -497
- package/telegram-plugin/card-event-log.ts +0 -138
- package/telegram-plugin/dist/foreman/foreman.js +0 -31106
- package/telegram-plugin/docs/multi-agent-card-design.md +0 -847
- package/telegram-plugin/docs/pinned-progress-card-reliability.md +0 -144
- package/telegram-plugin/foreman/foreman-create-flow.ts +0 -202
- package/telegram-plugin/foreman/foreman-handlers.ts +0 -493
- package/telegram-plugin/foreman/foreman.ts +0 -1165
- package/telegram-plugin/foreman/setup-flow.ts +0 -345
- package/telegram-plugin/foreman/setup-state.ts +0 -239
- package/telegram-plugin/foreman/state.ts +0 -203
- package/telegram-plugin/pin-event-log.ts +0 -76
- package/telegram-plugin/progress-card-driver.ts +0 -2886
- package/telegram-plugin/progress-card-pin-manager.ts +0 -589
- package/telegram-plugin/progress-card-pin-watchdog.ts +0 -98
- package/telegram-plugin/progress-card.ts +0 -1409
- package/telegram-plugin/tests/HARNESS.md +0 -340
- package/telegram-plugin/tests/_progress-card-harness.ts +0 -109
- package/telegram-plugin/tests/active-pins-boot-reaper.test.ts +0 -211
- package/telegram-plugin/tests/active-pins-sweep.test.ts +0 -309
- package/telegram-plugin/tests/active-pins.test.ts +0 -187
- package/telegram-plugin/tests/auth-account-identity-surface.test.ts +0 -118
- package/telegram-plugin/tests/auth-dashboard-edge-cases.test.ts +0 -260
- package/telegram-plugin/tests/auth-dashboard-restart-flow.test.ts +0 -140
- package/telegram-plugin/tests/auth-dashboard-v3b.test.ts +0 -559
- package/telegram-plugin/tests/auth-dashboard.test.ts +0 -1045
- package/telegram-plugin/tests/auth-slot-commands.test.ts +0 -640
- package/telegram-plugin/tests/bg-agent-progress-card-757.test.ts +0 -201
- package/telegram-plugin/tests/boot-card-account-quota.test.ts +0 -137
- package/telegram-plugin/tests/card-event-log.test.ts +0 -145
- package/telegram-plugin/tests/first-paint.test.ts +0 -257
- package/telegram-plugin/tests/foreman-create-flow.test.ts +0 -359
- package/telegram-plugin/tests/foreman-handlers.test.ts +0 -347
- package/telegram-plugin/tests/foreman-state.test.ts +0 -164
- package/telegram-plugin/tests/foreman-write-ops.test.ts +0 -214
- package/telegram-plugin/tests/harness-ordering-invariants.test.ts +0 -243
- package/telegram-plugin/tests/pin-event-log.test.ts +0 -124
- package/telegram-plugin/tests/progress-card-api-failure-during-deferred.test.ts +0 -73
- package/telegram-plugin/tests/progress-card-close-paths-converge.test.ts +0 -272
- package/telegram-plugin/tests/progress-card-cross-turn.test.ts +0 -258
- package/telegram-plugin/tests/progress-card-delay-842.test.ts +0 -160
- package/telegram-plugin/tests/progress-card-dispose-preservepending.test.ts +0 -81
- package/telegram-plugin/tests/progress-card-draft-flag.test.ts +0 -80
- package/telegram-plugin/tests/progress-card-driver-eviction.test.ts +0 -215
- package/telegram-plugin/tests/progress-card-driver-fleet-shadow.test.ts +0 -123
- package/telegram-plugin/tests/progress-card-driver-force-complete-parent-done.test.ts +0 -76
- package/telegram-plugin/tests/progress-card-edit-timestamps-budget.test.ts +0 -62
- package/telegram-plugin/tests/progress-card-memory-bounds.test.ts +0 -84
- package/telegram-plugin/tests/progress-card-pin-failure-paths.test.ts +0 -139
- package/telegram-plugin/tests/progress-card-pin-manager.test.ts +0 -773
- package/telegram-plugin/tests/progress-card-pin-race-fast-turn.test.ts +0 -66
- package/telegram-plugin/tests/progress-card-pin-sidecar-partial-write.test.ts +0 -64
- package/telegram-plugin/tests/progress-card-pin-watchdog.test.ts +0 -190
- package/telegram-plugin/tests/progress-card-sigterm-pin-flush.test.ts +0 -146
- package/telegram-plugin/tests/real-gateway-f1-ladder-integrity.test.ts +0 -123
- package/telegram-plugin/tests/real-gateway-f2-instant-draft.test.ts +0 -82
- package/telegram-plugin/tests/real-gateway-f3-late-card.test.ts +0 -114
- package/telegram-plugin/tests/real-gateway-harness.ts +0 -699
- package/telegram-plugin/tests/real-gateway-i6-turn-flush-replay-dedup.test.ts +0 -313
- package/telegram-plugin/tests/real-gateway-ipc-lifecycle.test.ts +0 -299
- package/telegram-plugin/tests/real-gateway-spec.test.ts +0 -487
- package/telegram-plugin/tests/real-gateway.smoke.test.ts +0 -101
- package/telegram-plugin/tests/setup-flow.test.ts +0 -510
- package/telegram-plugin/tests/setup-state.test.ts +0 -146
- package/telegram-plugin/tests/sync-chat-running-subagents.test.ts +0 -116
- package/telegram-plugin/tests/turn-end-regressions.test.ts +0 -489
- package/telegram-plugin/tests/turn-flush-card-takeover.test.ts +0 -218
- package/telegram-plugin/tests/turn-flush-prose-recovery.test.ts +0 -78
- package/telegram-plugin/tests/two-zone-bg-carry-full-lifecycle.test.ts +0 -131
- package/telegram-plugin/tests/two-zone-bg-detection.test.ts +0 -120
- package/telegram-plugin/tests/two-zone-bg-done-when-all-terminal.test.ts +0 -116
- package/telegram-plugin/tests/two-zone-bg-early-turn-end.test.ts +0 -87
- package/telegram-plugin/tests/two-zone-bg-survives-next-turn.test.ts +0 -211
- package/telegram-plugin/tests/two-zone-card-cap.test.ts +0 -62
- package/telegram-plugin/tests/two-zone-card-fleet-row.test.ts +0 -101
- package/telegram-plugin/tests/two-zone-card-header-phases.test.ts +0 -78
- package/telegram-plugin/tests/two-zone-card-html-balance.test.ts +0 -110
- package/telegram-plugin/tests/two-zone-card-lifecycle.test.ts +0 -128
- package/telegram-plugin/tests/two-zone-card-sanitise.test.ts +0 -58
- package/telegram-plugin/tests/two-zone-card-snapshot.test.ts +0 -133
- package/telegram-plugin/tests/two-zone-concurrent-turns-isolation.test.ts +0 -155
- package/telegram-plugin/tests/two-zone-phasefor-precedence.test.ts +0 -117
- package/telegram-plugin/tests/two-zone-snapshot-extras.test.ts +0 -187
- package/telegram-plugin/tests/two-zone-stuck-edit-throttle.test.ts +0 -149
- package/telegram-plugin/tests/two-zone-stuck-header-escalation.test.ts +0 -101
- package/telegram-plugin/tests/two-zone-stuck-per-member.test.ts +0 -114
- package/telegram-plugin/tests/two-zone-stuck-recovery.test.ts +0 -105
- package/telegram-plugin/tests/waiting-ux-harness.ts +0 -381
- package/telegram-plugin/tests/waiting-ux.e2e.test.ts +0 -233
- package/telegram-plugin/turn-flush-prose-recovery.ts +0 -40
- package/telegram-plugin/two-zone-card.ts +0 -269
- package/telegram-plugin/uat/scenarios/smoke-clerk-reply.test.ts +0 -61
|
@@ -3,11 +3,20 @@
|
|
|
3
3
|
*
|
|
4
4
|
* Issue: https://github.com/switchroom/switchroom/issues/866
|
|
5
5
|
*
|
|
6
|
-
* `spinUp({ agent
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
6
|
+
* `spinUp({ agent })` connects the mtcute driver and resolves the test
|
|
7
|
+
* bot's user_id, returning a Scenario the test can interact with.
|
|
8
|
+
*
|
|
9
|
+
* **Runtime model — Phase 2a (DM focus):** the test-harness agent is
|
|
10
|
+
* a standard switchroom agent the operator created once via
|
|
11
|
+
* `switchroom agent add test-harness ...` (see uat/SETUP.md). The
|
|
12
|
+
* harness does NOT spin the agent up per-scenario — it relies on the
|
|
13
|
+
* agent being already running. Per-scenario state isolation rolls in
|
|
14
|
+
* with Phase 2b once we move beyond DM smoke tests.
|
|
15
|
+
*
|
|
16
|
+
* Forum-topic routing, ephemeral STATE_DIR, child-process agents, and
|
|
17
|
+
* the progress-card observers are deferred to Phase 2b (#866 v2) — the
|
|
18
|
+
* epic's original plan was written before the Docker runtime landed
|
|
19
|
+
* and would substantially re-invent the agent lifecycle.
|
|
11
20
|
*/
|
|
12
21
|
|
|
13
22
|
import { Driver } from "./driver.js";
|
|
@@ -19,32 +28,65 @@ import {
|
|
|
19
28
|
type PollOptions,
|
|
20
29
|
type PinnedCardSnapshot,
|
|
21
30
|
} from "./assertions.js";
|
|
22
|
-
import {
|
|
31
|
+
import type { ObservedMessage } from "./driver.js";
|
|
32
|
+
import { loadUatEnv } from "./load-env.js";
|
|
33
|
+
|
|
34
|
+
loadUatEnv();
|
|
23
35
|
|
|
24
36
|
export interface SpinUpOptions {
|
|
25
|
-
/**
|
|
37
|
+
/**
|
|
38
|
+
* Agent name to run scenarios against, e.g. `"test-harness"`. The
|
|
39
|
+
* agent must already be configured + running (Phase 2a: standard
|
|
40
|
+
* runtime + persistent agent).
|
|
41
|
+
*/
|
|
26
42
|
agent: string;
|
|
27
43
|
/**
|
|
28
|
-
*
|
|
29
|
-
*
|
|
44
|
+
* Bot username (with or without `@`) the harness should resolve to
|
|
45
|
+
* a user_id. Defaults to `process.env.TELEGRAM_TEST_BOT_USERNAME`.
|
|
46
|
+
*/
|
|
47
|
+
botUsername?: string;
|
|
48
|
+
/**
|
|
49
|
+
* Settle delay (ms) after the driver connects, before the scenario's
|
|
50
|
+
* first send. Gives the previous scenario's turn time to finish its
|
|
51
|
+
* outbound stream on the agent side. Without this the next inbound
|
|
52
|
+
* lands while the gateway is still pinning/editing the prior turn's
|
|
53
|
+
* card, the gateway reuses the existing pin via edit (instead of
|
|
54
|
+
* pinning a new message), and observePins-based assertions miss the
|
|
55
|
+
* event entirely. Default {@link DEFAULT_SETTLE_MS}; set to 0 for
|
|
56
|
+
* single-scenario runs where the cooldown is dead time. Scenarios
|
|
57
|
+
* that account for this in their outer `it()` budget should add the
|
|
58
|
+
* settle on top of inner poll deadlines.
|
|
30
59
|
*/
|
|
31
|
-
|
|
60
|
+
settleMs?: number;
|
|
32
61
|
}
|
|
33
62
|
|
|
63
|
+
export const DEFAULT_SETTLE_MS = 8_000;
|
|
64
|
+
|
|
34
65
|
export interface Scenario {
|
|
35
66
|
/** mtcute driver, already connected. */
|
|
36
67
|
driver: Driver;
|
|
37
|
-
/**
|
|
38
|
-
|
|
39
|
-
/**
|
|
40
|
-
|
|
68
|
+
/** Test bot's Telegram user_id; doubles as the chat_id for DMs. */
|
|
69
|
+
botUserId: number;
|
|
70
|
+
/** Driver user account's Telegram user_id. */
|
|
71
|
+
driverUserId: number;
|
|
41
72
|
|
|
42
|
-
|
|
43
|
-
|
|
73
|
+
/** Sugar for `driver.sendText(botUserId, text)`. */
|
|
74
|
+
sendDM: (text: string) => Promise<{ messageId: number }>;
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Wait for the next message in the bot DM chat matching `match`.
|
|
78
|
+
* `opts.from` filters by sender side: `"bot"` for replies from the
|
|
79
|
+
* test bot, `"driver"` for the driver's own echoes (rare in
|
|
80
|
+
* scenarios but useful for assertions on the outbound side).
|
|
81
|
+
*/
|
|
44
82
|
expectMessage: (
|
|
45
|
-
match:
|
|
46
|
-
opts: PollOptions & { from?: "bot" | "
|
|
47
|
-
) =>
|
|
83
|
+
match: string | RegExp | ((m: ObservedMessage) => boolean),
|
|
84
|
+
opts: PollOptions & { from?: "bot" | "driver" },
|
|
85
|
+
) => Promise<ObservedMessage>;
|
|
86
|
+
|
|
87
|
+
// Phase 2b stubs — type-only so existing scenarios that reference
|
|
88
|
+
// these helpers still typecheck after this PR. Implementations land
|
|
89
|
+
// alongside `observeReactions` / `observePins` in #866 v2.
|
|
48
90
|
expectReaction: (
|
|
49
91
|
messageId: number,
|
|
50
92
|
sequence: string[],
|
|
@@ -57,105 +99,109 @@ export interface Scenario {
|
|
|
57
99
|
opts: PollOptions,
|
|
58
100
|
) => ReturnType<typeof waitForCardPhase>;
|
|
59
101
|
|
|
60
|
-
/**
|
|
102
|
+
/** Disconnect the driver. The persistent test-harness agent keeps running. */
|
|
61
103
|
tearDown: () => Promise<void>;
|
|
62
104
|
}
|
|
63
105
|
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
106
|
+
interface ResolvedConfig {
|
|
107
|
+
apiId: number;
|
|
108
|
+
apiHash: string;
|
|
109
|
+
session: string;
|
|
110
|
+
botUsername: string;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function resolveConfig(opts: SpinUpOptions): ResolvedConfig {
|
|
114
|
+
const apiId = Number.parseInt(process.env.TELEGRAM_API_ID ?? "", 10);
|
|
115
|
+
if (!Number.isFinite(apiId)) {
|
|
116
|
+
fail("TELEGRAM_API_ID is missing or not an integer — see uat/SETUP.md §3");
|
|
117
|
+
}
|
|
118
|
+
const apiHash = process.env.TELEGRAM_API_HASH ?? "";
|
|
119
|
+
if (apiHash.length === 0) {
|
|
120
|
+
fail("TELEGRAM_API_HASH is empty — see uat/SETUP.md §3");
|
|
121
|
+
}
|
|
122
|
+
const session = process.env.TELEGRAM_UAT_DRIVER_SESSION ?? "";
|
|
123
|
+
if (session.length === 0) {
|
|
124
|
+
fail(
|
|
125
|
+
"TELEGRAM_UAT_DRIVER_SESSION is empty — run `bun run uat:login` first " +
|
|
126
|
+
"(see uat/SETUP.md §4)",
|
|
82
127
|
);
|
|
83
128
|
}
|
|
84
|
-
const
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
129
|
+
const botUsername =
|
|
130
|
+
opts.botUsername ?? process.env.TELEGRAM_TEST_BOT_USERNAME ?? "";
|
|
131
|
+
if (botUsername.length === 0) {
|
|
132
|
+
fail(
|
|
133
|
+
"Bot username not provided — pass `botUsername` to spinUp() or set " +
|
|
134
|
+
"TELEGRAM_TEST_BOT_USERNAME",
|
|
88
135
|
);
|
|
89
136
|
}
|
|
137
|
+
return { apiId, apiHash, session, botUsername };
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
export async function spinUp(opts: SpinUpOptions): Promise<Scenario> {
|
|
141
|
+
const cfg = resolveConfig(opts);
|
|
142
|
+
void opts.agent; // currently informational; #866 v2 will use it for state-dir scoping
|
|
90
143
|
|
|
91
|
-
// TODO(#866): allocate gateway port + ephemeral STATE_DIR.
|
|
92
|
-
// const port = await allocatePort();
|
|
93
|
-
// const stateDir = await mkdtemp(join(tmpdir(), `uat-${opts.agent}-`));
|
|
94
|
-
// process.env.STATE_DIR is per-process — we instead pass STATE_DIR
|
|
95
|
-
// in the spawned child's env, never mutate ours.
|
|
96
|
-
const port = await allocatePort();
|
|
97
|
-
void port; // Phase 2: feed into agent child env
|
|
98
|
-
|
|
99
|
-
// TODO(#866): create the forum topic via Bot API
|
|
100
|
-
// (`createForumTopic`) using the test bot token; capture the
|
|
101
|
-
// returned `message_thread_id` and stash for tearDown's
|
|
102
|
-
// `deleteForumTopic`.
|
|
103
|
-
const threadId = -1; // sentinel; Phase 2 fills in
|
|
104
|
-
|
|
105
|
-
// TODO(#866): spawn the agent under test as a child process.
|
|
106
|
-
// const child = spawn(process.execPath, [agentEntry], {
|
|
107
|
-
// env: {
|
|
108
|
-
// ...process.env,
|
|
109
|
-
// STATE_DIR: stateDir,
|
|
110
|
-
// TELEGRAM_GATEWAY_PORT: String(port),
|
|
111
|
-
// SWITCHROOM_AGENT_NAME: opts.agent,
|
|
112
|
-
// BOT_TOKEN: <vault: telegram-test-bot-token>,
|
|
113
|
-
// },
|
|
114
|
-
// stdio: ["ignore", "pipe", "pipe"],
|
|
115
|
-
// });
|
|
116
|
-
// await waitForGatewayReady(port, { timeout: 30_000 });
|
|
117
|
-
|
|
118
|
-
// TODO(#866): connect mtcute driver.
|
|
119
|
-
// const driver = new Driver({ apiId, apiHash, session });
|
|
120
|
-
// await driver.connect();
|
|
121
144
|
const driver = new Driver({
|
|
122
|
-
apiId:
|
|
123
|
-
apiHash:
|
|
124
|
-
session:
|
|
145
|
+
apiId: cfg.apiId,
|
|
146
|
+
apiHash: cfg.apiHash,
|
|
147
|
+
session: cfg.session,
|
|
125
148
|
});
|
|
126
149
|
|
|
150
|
+
await driver.connect();
|
|
151
|
+
|
|
152
|
+
// Resolve both IDs eagerly so scenarios can rely on them being
|
|
153
|
+
// populated by the time `spinUp` returns. Run in parallel — the
|
|
154
|
+
// two calls don't interact.
|
|
155
|
+
const [botUserId, driverUserId] = await Promise.all([
|
|
156
|
+
driver.resolveBotUserId(cfg.botUsername),
|
|
157
|
+
driver.getMyUserId(),
|
|
158
|
+
]);
|
|
159
|
+
|
|
160
|
+
// Unpin FIRST, then settle. Order matters: the gateway is a logical
|
|
161
|
+
// singleton for the chat's pinned card — on every turn it tries to
|
|
162
|
+
// edit the existing pin rather than pin a fresh one, so observePins
|
|
163
|
+
// (a transition listener) sees nothing on the next turn. Unpinning
|
|
164
|
+
// forces the agent to issue a fresh `pin` event we can observe.
|
|
165
|
+
// The settle delay then absorbs (a) the unpin's own propagation
|
|
166
|
+
// round-trip and (b) any tail-end edits from the prior scenario's
|
|
167
|
+
// turn still in flight. Doing unpin before settle keeps the gap a
|
|
168
|
+
// single window of dead time rather than two stacked waits.
|
|
169
|
+
await driver.unpinAllMessages(botUserId);
|
|
170
|
+
const settleMs = opts.settleMs ?? DEFAULT_SETTLE_MS;
|
|
171
|
+
if (settleMs > 0) {
|
|
172
|
+
await new Promise((resolve) => setTimeout(resolve, settleMs));
|
|
173
|
+
}
|
|
174
|
+
|
|
127
175
|
const scenario: Scenario = {
|
|
128
176
|
driver,
|
|
129
|
-
|
|
130
|
-
|
|
177
|
+
botUserId,
|
|
178
|
+
driverUserId,
|
|
179
|
+
sendDM: (text) => driver.sendText(botUserId, text),
|
|
131
180
|
expectMessage: (match, pollOpts) =>
|
|
132
|
-
expectMessage(driver,
|
|
181
|
+
expectMessage(driver, botUserId, match, {
|
|
133
182
|
...pollOpts,
|
|
134
|
-
|
|
183
|
+
senderFilter:
|
|
184
|
+
pollOpts.from === "bot"
|
|
185
|
+
? { notUserId: driverUserId }
|
|
186
|
+
: pollOpts.from === "driver"
|
|
187
|
+
? { userId: driverUserId }
|
|
188
|
+
: undefined,
|
|
135
189
|
}),
|
|
136
190
|
expectReaction: (messageId, sequence, pollOpts) =>
|
|
137
|
-
expectReaction(driver,
|
|
138
|
-
expectPinnedCard: (pollOpts) =>
|
|
139
|
-
expectPinnedCard(driver, chatId, { ...pollOpts, threadId }),
|
|
191
|
+
expectReaction(driver, botUserId, messageId, sequence, pollOpts),
|
|
192
|
+
expectPinnedCard: (pollOpts) => expectPinnedCard(driver, botUserId, pollOpts),
|
|
140
193
|
waitForCardPhase: (card, phase, pollOpts) =>
|
|
141
194
|
waitForCardPhase(driver, card, phase, pollOpts),
|
|
142
195
|
tearDown: async () => {
|
|
143
|
-
// TODO(#866): SIGTERM child, await exit (or SIGKILL after 5s),
|
|
144
|
-
// delete forum topic, rm -rf state dir, disconnect driver.
|
|
145
196
|
await driver.disconnect().catch(() => {
|
|
146
|
-
/* idempotent */
|
|
197
|
+
/* idempotent — log-and-move-on; the persistent agent is unaffected */
|
|
147
198
|
});
|
|
148
199
|
},
|
|
149
200
|
};
|
|
150
201
|
|
|
151
|
-
// Phase 1 marker so accidental runs fail loudly instead of silently
|
|
152
|
-
// sending nothing.
|
|
153
|
-
void opts;
|
|
154
|
-
throw new Error(
|
|
155
|
-
"[uat/harness] spinUp is scaffolded but not wired (Phase 1 stub) — see TODO markers in uat/harness.ts",
|
|
156
|
-
);
|
|
157
|
-
|
|
158
|
-
// unreachable in Phase 1; left for shape:
|
|
159
|
-
// eslint-disable-next-line no-unreachable
|
|
160
202
|
return scenario;
|
|
161
203
|
}
|
|
204
|
+
|
|
205
|
+
function fail(msg: string): never {
|
|
206
|
+
throw new Error(`[uat/harness] ${msg}`);
|
|
207
|
+
}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import { afterEach, beforeEach, describe, expect, it } from "bun:test";
|
|
2
|
+
import { mkdtempSync, rmSync, writeFileSync } from "node:fs";
|
|
3
|
+
import { tmpdir } from "node:os";
|
|
4
|
+
import { join } from "node:path";
|
|
5
|
+
import { loadUatEnv } from "./load-env.js";
|
|
6
|
+
|
|
7
|
+
describe("loadUatEnv", () => {
|
|
8
|
+
let tmpDir: string;
|
|
9
|
+
let envFile: string;
|
|
10
|
+
const originalEnv = { ...process.env };
|
|
11
|
+
|
|
12
|
+
beforeEach(() => {
|
|
13
|
+
tmpDir = mkdtempSync(join(tmpdir(), "uat-load-env-"));
|
|
14
|
+
envFile = join(tmpDir, ".env");
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
afterEach(() => {
|
|
18
|
+
rmSync(tmpDir, { recursive: true, force: true });
|
|
19
|
+
for (const key of Object.keys(process.env)) {
|
|
20
|
+
if (!(key in originalEnv)) delete process.env[key];
|
|
21
|
+
}
|
|
22
|
+
for (const [key, value] of Object.entries(originalEnv)) {
|
|
23
|
+
process.env[key] = value;
|
|
24
|
+
}
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
it("populates env vars from KEY=value lines", () => {
|
|
28
|
+
writeFileSync(envFile, "UAT_TEST_FOO=bar\nUAT_TEST_BAZ=qux\n");
|
|
29
|
+
loadUatEnv(envFile);
|
|
30
|
+
expect(process.env.UAT_TEST_FOO).toBe("bar");
|
|
31
|
+
expect(process.env.UAT_TEST_BAZ).toBe("qux");
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
it("does not overwrite values already set in process.env", () => {
|
|
35
|
+
process.env.UAT_TEST_FOO = "from-shell";
|
|
36
|
+
writeFileSync(envFile, "UAT_TEST_FOO=from-file\n");
|
|
37
|
+
loadUatEnv(envFile);
|
|
38
|
+
expect(process.env.UAT_TEST_FOO).toBe("from-shell");
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
it("strips surrounding single or double quotes", () => {
|
|
42
|
+
writeFileSync(envFile, `UAT_TEST_DQ="quoted"\nUAT_TEST_SQ='quoted'\n`);
|
|
43
|
+
loadUatEnv(envFile);
|
|
44
|
+
expect(process.env.UAT_TEST_DQ).toBe("quoted");
|
|
45
|
+
expect(process.env.UAT_TEST_SQ).toBe("quoted");
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
it("ignores blank lines and # comments", () => {
|
|
49
|
+
writeFileSync(envFile, "# top comment\n\nUAT_TEST_FOO=bar\n# trailing\n");
|
|
50
|
+
loadUatEnv(envFile);
|
|
51
|
+
expect(process.env.UAT_TEST_FOO).toBe("bar");
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
it("is a no-op when the file does not exist", () => {
|
|
55
|
+
const before = process.env.UAT_TEST_NONEXISTENT;
|
|
56
|
+
loadUatEnv(join(tmpDir, "missing.env"));
|
|
57
|
+
expect(process.env.UAT_TEST_NONEXISTENT).toBe(before);
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
it("handles values containing = signs (session strings)", () => {
|
|
61
|
+
writeFileSync(envFile, "UAT_TEST_SESSION=abc=def=ghi\n");
|
|
62
|
+
loadUatEnv(envFile);
|
|
63
|
+
expect(process.env.UAT_TEST_SESSION).toBe("abc=def=ghi");
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
it("skips empty values so unpopulated .env.example copies stay unset", () => {
|
|
67
|
+
writeFileSync(envFile, "UAT_TEST_EMPTY=\nUAT_TEST_QUOTED_EMPTY=\"\"\n");
|
|
68
|
+
loadUatEnv(envFile);
|
|
69
|
+
expect(process.env.UAT_TEST_EMPTY).toBeUndefined();
|
|
70
|
+
expect(process.env.UAT_TEST_QUOTED_EMPTY).toBeUndefined();
|
|
71
|
+
});
|
|
72
|
+
});
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Load the repo-root `.env` into process.env at harness startup.
|
|
3
|
+
*
|
|
4
|
+
* The UAT harness needs four env vars (TELEGRAM_API_ID, TELEGRAM_API_HASH,
|
|
5
|
+
* TELEGRAM_UAT_DRIVER_SESSION, TELEGRAM_TEST_BOT_USERNAME) that originate
|
|
6
|
+
* in the operator's vault. Re-exporting them every shell session is fiddly,
|
|
7
|
+
* so we let the operator stash them in a gitignored repo-root `.env`.
|
|
8
|
+
* Consolidated at the repo root (previously lived at
|
|
9
|
+
* `telegram-plugin/uat/.env`) so a single file handles UAT secrets +
|
|
10
|
+
* any future repo-wide dev env knobs. See `SETUP.md` §6 for the
|
|
11
|
+
* refresh workflow.
|
|
12
|
+
*
|
|
13
|
+
* Existing process.env values win — a CI run that supplies vars via the
|
|
14
|
+
* job environment doesn't get clobbered by a stale local `.env`.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
18
|
+
import path from "node:path";
|
|
19
|
+
import { fileURLToPath } from "node:url";
|
|
20
|
+
|
|
21
|
+
const HERE = path.dirname(fileURLToPath(import.meta.url));
|
|
22
|
+
// HERE is `<repo>/telegram-plugin/uat` (or the dist-mirror); two
|
|
23
|
+
// dirname() hops up gets us the repo root regardless of where the
|
|
24
|
+
// bundled output lives.
|
|
25
|
+
export const UAT_ENV_FILE = path.resolve(HERE, "..", "..", ".env");
|
|
26
|
+
|
|
27
|
+
export function loadUatEnv(envPath: string = UAT_ENV_FILE): void {
|
|
28
|
+
if (!existsSync(envPath)) return;
|
|
29
|
+
const content = readFileSync(envPath, "utf-8");
|
|
30
|
+
for (const rawLine of content.split("\n")) {
|
|
31
|
+
const line = rawLine.trim();
|
|
32
|
+
if (line === "" || line.startsWith("#")) continue;
|
|
33
|
+
const eq = line.indexOf("=");
|
|
34
|
+
if (eq === -1) continue;
|
|
35
|
+
const key = line.slice(0, eq).trim();
|
|
36
|
+
let value = line.slice(eq + 1).trim();
|
|
37
|
+
if (
|
|
38
|
+
(value.startsWith('"') && value.endsWith('"')) ||
|
|
39
|
+
(value.startsWith("'") && value.endsWith("'"))
|
|
40
|
+
) {
|
|
41
|
+
value = value.slice(1, -1);
|
|
42
|
+
}
|
|
43
|
+
if (value === "") continue;
|
|
44
|
+
if (process.env[key] === undefined) {
|
|
45
|
+
process.env[key] = value;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
}
|
|
@@ -3,9 +3,9 @@
|
|
|
3
3
|
*
|
|
4
4
|
* Issue: https://github.com/switchroom/switchroom/issues/865
|
|
5
5
|
*
|
|
6
|
-
* Run via `bun run uat:login` from telegram-plugin
|
|
7
|
-
* phone, login code, and
|
|
8
|
-
*
|
|
6
|
+
* Run via `bun run uat:login` from `telegram-plugin/`. Prompts for
|
|
7
|
+
* phone, login code, and 2FA password on stdin. Captures the session
|
|
8
|
+
* string in memory and writes it to vault under
|
|
9
9
|
* `telegram-uat-driver-session`. The session string is **never
|
|
10
10
|
* printed** — not to stdout, not to stderr, not to logs. If you see
|
|
11
11
|
* one in scrollback, file an incident.
|
|
@@ -13,14 +13,29 @@
|
|
|
13
13
|
* Required env:
|
|
14
14
|
* TELEGRAM_API_ID — from https://my.telegram.org/apps
|
|
15
15
|
* TELEGRAM_API_HASH — from https://my.telegram.org/apps
|
|
16
|
+
*
|
|
17
|
+
* Vault write: the script writes the session into a 0600 tmpfile and
|
|
18
|
+
* spawns `switchroom vault set --file <tmpf> --allow test-harness`
|
|
19
|
+
* with inherited stdio. The operator is prompted once for the vault
|
|
20
|
+
* passphrase (the broker-mediated stdin path doesn't support `--allow`
|
|
21
|
+
* scope flags; see `src/cli/vault.ts:331-356` for the rationale). The
|
|
22
|
+
* tmpfile is `shred -u`'d after the spawn returns.
|
|
16
23
|
*/
|
|
17
24
|
|
|
18
25
|
import { spawn } from "node:child_process";
|
|
26
|
+
import { mkdtemp, rm, writeFile } from "node:fs/promises";
|
|
27
|
+
import { tmpdir } from "node:os";
|
|
28
|
+
import { join } from "node:path";
|
|
19
29
|
import { createInterface } from "node:readline/promises";
|
|
20
30
|
import { stdin as input, stdout as output } from "node:process";
|
|
21
|
-
import {
|
|
31
|
+
import { fileURLToPath } from "node:url";
|
|
32
|
+
import { MemoryStorage, TelegramClient } from "@mtcute/node";
|
|
33
|
+
import { loadUatEnv } from "./load-env.js";
|
|
34
|
+
|
|
35
|
+
loadUatEnv();
|
|
22
36
|
|
|
23
|
-
const VAULT_KEY = "telegram-uat-driver-session";
|
|
37
|
+
export const VAULT_KEY = "telegram-uat-driver-session";
|
|
38
|
+
export const VAULT_SCOPE = "test-harness";
|
|
24
39
|
|
|
25
40
|
async function main(): Promise<void> {
|
|
26
41
|
const apiId = Number.parseInt(process.env.TELEGRAM_API_ID ?? "", 10);
|
|
@@ -33,8 +48,6 @@ async function main(): Promise<void> {
|
|
|
33
48
|
|
|
34
49
|
const rl = createInterface({ input, output, terminal: true });
|
|
35
50
|
|
|
36
|
-
// Confirm the operator understands the security posture before we
|
|
37
|
-
// mint a bearer-equivalent credential.
|
|
38
51
|
const ack = await rl.question(
|
|
39
52
|
[
|
|
40
53
|
"",
|
|
@@ -52,12 +65,15 @@ async function main(): Promise<void> {
|
|
|
52
65
|
const phone = (await rl.question("Phone number (E.164, e.g. +14155551234): ")).trim();
|
|
53
66
|
if (!phone.startsWith("+")) fail("Phone must start with '+'.");
|
|
54
67
|
|
|
55
|
-
|
|
68
|
+
// MemoryStorage so nothing lands on disk in this process. The
|
|
69
|
+
// exported session string is the only durable output; everything
|
|
70
|
+
// else is ephemeral.
|
|
71
|
+
const client = new TelegramClient({
|
|
72
|
+
apiId,
|
|
73
|
+
apiHash,
|
|
74
|
+
storage: new MemoryStorage(),
|
|
75
|
+
});
|
|
56
76
|
|
|
57
|
-
// mtcute exposes a `start()` flow that takes async callbacks for
|
|
58
|
-
// each interactive step. Exact callback names may shift across
|
|
59
|
-
// versions — verify against the pinned mtcute version before first
|
|
60
|
-
// run.
|
|
61
77
|
await client.start({
|
|
62
78
|
phone: async () => phone,
|
|
63
79
|
code: async () =>
|
|
@@ -66,54 +82,73 @@ async function main(): Promise<void> {
|
|
|
66
82
|
(await rl.question("2FA password (leave blank if none): ")),
|
|
67
83
|
});
|
|
68
84
|
|
|
69
|
-
|
|
70
|
-
// `@mtcute/core/utils.js` `StringSessionStorage` adapter; the
|
|
71
|
-
// exact call is `await client.exportSession()` only when that
|
|
72
|
-
// storage is configured, otherwise sessions live in the SQLite
|
|
73
|
-
// file at `client.session`. Phase 2 wires the string-session
|
|
74
|
-
// storage so this script can mint a string. For now we throw
|
|
75
|
-
// before producing a value so the operator never gets a half-
|
|
76
|
-
// baked session in vault.
|
|
77
|
-
const session: string = await Promise.reject(
|
|
78
|
-
new Error(
|
|
79
|
-
"uat:login: Phase 1 stub — Phase 2 wires StringSessionStorage. See uat/SETUP.md §3.",
|
|
80
|
-
),
|
|
81
|
-
);
|
|
82
|
-
|
|
85
|
+
const session = await client.exportSession();
|
|
83
86
|
await client.destroy();
|
|
84
87
|
rl.close();
|
|
85
88
|
|
|
86
|
-
// Write to vault via the switchroom CLI's stdin path so the
|
|
87
|
-
// session never appears in argv (which would land in `ps` output).
|
|
88
89
|
await writeToVault(VAULT_KEY, session);
|
|
89
90
|
|
|
90
|
-
// Belt-and-suspenders: zero out the local copy.
|
|
91
|
-
scrub(session);
|
|
92
|
-
|
|
93
91
|
process.stdout.write(
|
|
94
|
-
`\nDone. Session stored in vault as \`${VAULT_KEY}
|
|
92
|
+
`\nDone. Session stored in vault as \`${VAULT_KEY}\` (scope: allow=${VAULT_SCOPE}).\n` +
|
|
95
93
|
"If you ever see the actual session string in your terminal, file an incident.\n",
|
|
96
94
|
);
|
|
97
95
|
}
|
|
98
96
|
|
|
99
|
-
|
|
97
|
+
/**
|
|
98
|
+
* Spawns `switchroom vault set --file <tmpf> --allow test-harness` so
|
|
99
|
+
* the operator passphrase prompt works (broker-mediated stdin writes
|
|
100
|
+
* reject `--allow`/`--deny`). The tmpfile is created 0700-mode dir,
|
|
101
|
+
* 0600 file, and `shred -u`'d after the set returns regardless of
|
|
102
|
+
* outcome.
|
|
103
|
+
*
|
|
104
|
+
* Exported so `tests/uat-login.test.ts` can pin the security-critical
|
|
105
|
+
* invariants (mode 0600, `--allow test-harness`, cleanup on failure)
|
|
106
|
+
* against the real implementation.
|
|
107
|
+
*/
|
|
108
|
+
export async function writeToVault(key: string, value: string): Promise<void> {
|
|
109
|
+
const dir = await mkdtemp(join(tmpdir(), "uat-session-"));
|
|
110
|
+
const path = join(dir, "session");
|
|
111
|
+
try {
|
|
112
|
+
await writeFile(path, value, { mode: 0o600 });
|
|
113
|
+
await runInherit("switchroom", [
|
|
114
|
+
"vault",
|
|
115
|
+
"set",
|
|
116
|
+
key,
|
|
117
|
+
"--file",
|
|
118
|
+
path,
|
|
119
|
+
"--format",
|
|
120
|
+
"string",
|
|
121
|
+
"--allow",
|
|
122
|
+
VAULT_SCOPE,
|
|
123
|
+
]);
|
|
124
|
+
} finally {
|
|
125
|
+
// Best-effort secure delete. `shred -u` first (overwrites then
|
|
126
|
+
// unlinks); fall back to plain rm if shred is missing.
|
|
127
|
+
await runQuiet("shred", ["-u", path]).catch(() => undefined);
|
|
128
|
+
await rm(dir, { recursive: true, force: true }).catch(() => undefined);
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function runInherit(cmd: string, args: string[]): Promise<void> {
|
|
100
133
|
return new Promise((resolve, reject) => {
|
|
101
|
-
const proc = spawn(
|
|
102
|
-
stdio: ["pipe", "inherit", "inherit"],
|
|
103
|
-
});
|
|
134
|
+
const proc = spawn(cmd, args, { stdio: "inherit" });
|
|
104
135
|
proc.on("error", reject);
|
|
105
136
|
proc.on("exit", (code) => {
|
|
106
137
|
if (code === 0) resolve();
|
|
107
|
-
else reject(new Error(
|
|
138
|
+
else reject(new Error(`${cmd} ${args[0] ?? ""} exited ${code}`));
|
|
108
139
|
});
|
|
109
|
-
proc.stdin.end(value + "\n");
|
|
110
140
|
});
|
|
111
141
|
}
|
|
112
142
|
|
|
113
|
-
function
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
143
|
+
function runQuiet(cmd: string, args: string[]): Promise<void> {
|
|
144
|
+
return new Promise((resolve, reject) => {
|
|
145
|
+
const proc = spawn(cmd, args, { stdio: "ignore" });
|
|
146
|
+
proc.on("error", reject);
|
|
147
|
+
proc.on("exit", (code) => {
|
|
148
|
+
if (code === 0) resolve();
|
|
149
|
+
else reject(new Error(`${cmd} exited ${code}`));
|
|
150
|
+
});
|
|
151
|
+
});
|
|
117
152
|
}
|
|
118
153
|
|
|
119
154
|
function fail(msg: string): never {
|
|
@@ -121,14 +156,22 @@ function fail(msg: string): never {
|
|
|
121
156
|
process.exit(1);
|
|
122
157
|
}
|
|
123
158
|
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
)
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
159
|
+
// Only run the interactive flow when invoked directly (`bun run
|
|
160
|
+
// uat:login`). Tests that `import` this module for `writeToVault`
|
|
161
|
+
// otherwise trigger the prompt-for-phone-number flow on every load.
|
|
162
|
+
const invokedDirectly =
|
|
163
|
+
process.argv[1] !== undefined &&
|
|
164
|
+
fileURLToPath(import.meta.url) === process.argv[1];
|
|
165
|
+
if (invokedDirectly) {
|
|
166
|
+
main().catch((err) => {
|
|
167
|
+
// Defensive: if mtcute throws, the error MAY contain the session
|
|
168
|
+
// string in some adapters. Strip anything that looks like a long
|
|
169
|
+
// base64 blob before printing.
|
|
170
|
+
const sanitized = String(err?.message ?? err).replace(
|
|
171
|
+
/[A-Za-z0-9+/=_-]{64,}/g,
|
|
172
|
+
"<redacted>",
|
|
173
|
+
);
|
|
174
|
+
process.stderr.write(`uat:login failed: ${sanitized}\n`);
|
|
175
|
+
process.exit(1);
|
|
176
|
+
});
|
|
177
|
+
}
|