@songsid/agend 2.1.8 → 2.1.10-beta.1

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.
Files changed (136) hide show
  1. package/README.md +8 -3
  2. package/README.zh-TW.md +11 -5
  3. package/dist/backend/claude-code.d.ts +35 -5
  4. package/dist/backend/claude-code.js +288 -17
  5. package/dist/backend/claude-code.js.map +1 -1
  6. package/dist/backend/codex.d.ts +13 -0
  7. package/dist/backend/codex.js +74 -2
  8. package/dist/backend/codex.js.map +1 -1
  9. package/dist/backend/credential-profile.d.ts +9 -0
  10. package/dist/backend/credential-profile.js +20 -0
  11. package/dist/backend/credential-profile.js.map +1 -1
  12. package/dist/backend/grok.js +19 -0
  13. package/dist/backend/grok.js.map +1 -1
  14. package/dist/backend/kiro.d.ts +0 -2
  15. package/dist/backend/kiro.js +8 -7
  16. package/dist/backend/kiro.js.map +1 -1
  17. package/dist/backend/muse.d.ts +6 -0
  18. package/dist/backend/muse.js +7 -1
  19. package/dist/backend/muse.js.map +1 -1
  20. package/dist/backend/types.d.ts +1 -1
  21. package/dist/channel/adapters/telegram.js +4 -3
  22. package/dist/channel/adapters/telegram.js.map +1 -1
  23. package/dist/channel/mcp-operation-response.js +5 -4
  24. package/dist/channel/mcp-operation-response.js.map +1 -1
  25. package/dist/channel/mcp-server.js +9 -0
  26. package/dist/channel/mcp-server.js.map +1 -1
  27. package/dist/channel/mcp-tools.js +1 -1
  28. package/dist/channel/mcp-tools.js.map +1 -1
  29. package/dist/channel/types.d.ts +7 -0
  30. package/dist/cli.js +150 -52
  31. package/dist/cli.js.map +1 -1
  32. package/dist/config-validator.js +24 -0
  33. package/dist/config-validator.js.map +1 -1
  34. package/dist/config.d.ts +1 -0
  35. package/dist/config.js +27 -1
  36. package/dist/config.js.map +1 -1
  37. package/dist/cross-instance-envelope.d.ts +7 -0
  38. package/dist/cross-instance-envelope.js +19 -1
  39. package/dist/cross-instance-envelope.js.map +1 -1
  40. package/dist/daemon.d.ts +112 -3
  41. package/dist/daemon.js +360 -41
  42. package/dist/daemon.js.map +1 -1
  43. package/dist/delivery-outbox.d.ts +9 -1
  44. package/dist/delivery-outbox.js +43 -27
  45. package/dist/delivery-outbox.js.map +1 -1
  46. package/dist/fleet-manager.d.ts +133 -5
  47. package/dist/fleet-manager.js +525 -86
  48. package/dist/fleet-manager.js.map +1 -1
  49. package/dist/forged-envelope-scan.d.ts +160 -0
  50. package/dist/forged-envelope-scan.js +309 -0
  51. package/dist/forged-envelope-scan.js.map +1 -0
  52. package/dist/general-knowledge/skills/persona-emoji/SKILL.md +5 -2
  53. package/dist/instance-lifecycle.d.ts +90 -6
  54. package/dist/instance-lifecycle.js +238 -30
  55. package/dist/instance-lifecycle.js.map +1 -1
  56. package/dist/locale.js +180 -8
  57. package/dist/locale.js.map +1 -1
  58. package/dist/login-controller.d.ts +38 -0
  59. package/dist/login-controller.js +236 -8
  60. package/dist/login-controller.js.map +1 -1
  61. package/dist/login-flows.d.ts +25 -1
  62. package/dist/login-flows.js +10 -2
  63. package/dist/login-flows.js.map +1 -1
  64. package/dist/ls-rows.d.ts +17 -0
  65. package/dist/ls-rows.js +73 -0
  66. package/dist/ls-rows.js.map +1 -1
  67. package/dist/outbound-handlers.d.ts +3 -0
  68. package/dist/outbound-handlers.js +17 -19
  69. package/dist/outbound-handlers.js.map +1 -1
  70. package/dist/outbound-schemas.d.ts +2 -0
  71. package/dist/outbound-schemas.js +1 -1
  72. package/dist/outbound-schemas.js.map +1 -1
  73. package/dist/paths.js +4 -2
  74. package/dist/paths.js.map +1 -1
  75. package/dist/pause-marker.d.ts +10 -1
  76. package/dist/pause-marker.js +26 -2
  77. package/dist/pause-marker.js.map +1 -1
  78. package/dist/routing-engine.d.ts +17 -2
  79. package/dist/routing-engine.js +48 -5
  80. package/dist/routing-engine.js.map +1 -1
  81. package/dist/service-installer.d.ts +8 -0
  82. package/dist/service-installer.js +36 -1
  83. package/dist/service-installer.js.map +1 -1
  84. package/dist/settings-api.d.ts +2 -0
  85. package/dist/settings-api.js +3 -1
  86. package/dist/settings-api.js.map +1 -1
  87. package/dist/setup-wizard.js +3 -1
  88. package/dist/setup-wizard.js.map +1 -1
  89. package/dist/status-emojis.d.ts +9 -1
  90. package/dist/status-emojis.js +11 -1
  91. package/dist/status-emojis.js.map +1 -1
  92. package/dist/target-queue-worker.d.ts +67 -0
  93. package/dist/target-queue-worker.js +76 -0
  94. package/dist/target-queue-worker.js.map +1 -0
  95. package/dist/tool-permissions.js +7 -2
  96. package/dist/tool-permissions.js.map +1 -1
  97. package/dist/topic-commands.js +23 -19
  98. package/dist/topic-commands.js.map +1 -1
  99. package/dist/transcript-sources.d.ts +30 -0
  100. package/dist/transcript-sources.js +78 -0
  101. package/dist/transcript-sources.js.map +1 -1
  102. package/dist/tunnel/cloudflared.d.ts +23 -5
  103. package/dist/tunnel/cloudflared.js +32 -13
  104. package/dist/tunnel/cloudflared.js.map +1 -1
  105. package/dist/tunnel/manager.d.ts +6 -0
  106. package/dist/tunnel/manager.js +60 -22
  107. package/dist/tunnel/manager.js.map +1 -1
  108. package/dist/tunnel/public-fetch.d.ts +30 -0
  109. package/dist/tunnel/public-fetch.js +156 -0
  110. package/dist/tunnel/public-fetch.js.map +1 -0
  111. package/dist/tunnel/types.d.ts +8 -2
  112. package/dist/tunnel/types.js +8 -2
  113. package/dist/tunnel/types.js.map +1 -1
  114. package/dist/types.d.ts +50 -1
  115. package/dist/types.js +9 -1
  116. package/dist/types.js.map +1 -1
  117. package/dist/ui/settings.html +2 -2
  118. package/dist/ui/view.html +14 -0
  119. package/dist/ui/web-terminal/terminal.html +1 -0
  120. package/dist/wake-coordinator.d.ts +128 -0
  121. package/dist/wake-coordinator.js +337 -0
  122. package/dist/wake-coordinator.js.map +1 -0
  123. package/dist/web-api.d.ts +3 -1
  124. package/dist/web-api.js +1 -1
  125. package/dist/web-api.js.map +1 -1
  126. package/dist/web-host-guard.d.ts +47 -0
  127. package/dist/web-host-guard.js +75 -0
  128. package/dist/web-host-guard.js.map +1 -0
  129. package/dist/web-terminal-http.d.ts +34 -0
  130. package/dist/web-terminal-http.js +63 -0
  131. package/dist/web-terminal-http.js.map +1 -1
  132. package/dist/web-terminal.d.ts +2 -0
  133. package/dist/web-terminal.js +3 -6
  134. package/dist/web-terminal.js.map +1 -1
  135. package/package.json +1 -1
  136. package/templates/systemd.service.ejs +7 -0
@@ -0,0 +1,160 @@
1
+ /**
2
+ * #995 (#856-B): post-hoc detection of fabricated peer envelopes.
3
+ *
4
+ * (A) lets an agent verify a peer message before acting; (C) instruments the
5
+ * transport. This module is the defense-in-depth fallback: scan a receiver's
6
+ * OWN kiro
7
+ * transcript for fleet-envelope shapes (`[from:<instance>] … (message_id:
8
+ * xmsg-…)`) and check each message_id against the durable delivery record
9
+ * (#856's `delivery_status` message_id selector). An id nobody delivered is
10
+ * not a transport glitch — the receiver's model fabricated the turn.
11
+ *
12
+ * Only kiro has a reader here: its conversation store
13
+ * (`~/.local/share/kiro-cli/data.sqlite3`, conversations_v2) is on disk.
14
+ * Claude jsonl / Codex rollouts / other backends are per-backend follow-ups.
15
+ *
16
+ * Everything query-side is read-only: the kiro store opens readonly and the
17
+ * outbox is read through `DeliveryOutbox.queryStatusReadOnly` (operator
18
+ * semantics — existence only, content stays redacted).
19
+ */
20
+ import type { InstanceConfig } from "./types.js";
21
+ export interface EnvelopeCandidate {
22
+ /** Resolved real instance name (display-name wrapper stripped). */
23
+ fromInstance: string;
24
+ messageId: string;
25
+ /** Short single-line window around the match, for operator review. */
26
+ excerpt: string;
27
+ }
28
+ export interface ForgedEnvelopeFinding extends EnvelopeCandidate {
29
+ /** Exact text to inject into the instance (via send_to_instance/steer). */
30
+ warning: string;
31
+ }
32
+ export type CandidateVerdict = "delivered" | "forged" | "unverifiable";
33
+ /**
34
+ * Find fleet-envelope candidates in one assistant text. Each message_id is
35
+ * paired with the nearest `[from:]` tag naming a real instance within
36
+ * MAX_ENVELOPE_SPAN characters (preceding preferred); pairs are deduped.
37
+ */
38
+ export declare function extractEnvelopeCandidates(text: string, knownInstances: Set<string>): EnvelopeCandidate[];
39
+ export type KiroScanRead = {
40
+ kind: "ok";
41
+ conversationId: string;
42
+ texts: string[];
43
+ } | {
44
+ kind: "no-conversation";
45
+ } | {
46
+ kind: "unreadable";
47
+ reason: string;
48
+ };
49
+ /**
50
+ * Assistant texts the kiro store holds for one instance conversation.
51
+ * Discriminated (#1007): a missing/unreadable store is NOT an empty
52
+ * transcript — the caller must not report it as clean.
53
+ */
54
+ export declare function readKiroAssistantTexts(workingDirectory: string, kiroDbPath?: string): KiroScanRead;
55
+ /**
56
+ * Verify candidates against the durable record — the same oracle (A) agents
57
+ * use before destructive ops. Missing outbox DB (or an unreadable one) is
58
+ * `unverifiable`, never forged: absence of evidence is not fabrication.
59
+ */
60
+ export declare function verifyEnvelopeCandidates(outboxDbPath: string, messageIds: string[]): Map<string, CandidateVerdict>;
61
+ /**
62
+ * The exact correction to inject into the instance (send_to_instance or a
63
+ * mid-turn steer): the message it acted on was never fleet-delivered.
64
+ */
65
+ export declare function formatForgedEnvelopeWarning(instanceName: string, finding: EnvelopeCandidate): string;
66
+ export type KiroForgedScanStatus = "clean" | "forged-envelopes" | "no-conversation" | "unreadable-store" | "partially-unverified";
67
+ export interface KiroForgedScanResult {
68
+ instanceName: string;
69
+ status: KiroForgedScanStatus;
70
+ /** Why the store could not be read (unreadable-store only). */
71
+ reason?: string;
72
+ conversationId: string | null;
73
+ /** Envelope candidates found in the transcript (any verdict). */
74
+ checked: number;
75
+ delivered: number;
76
+ unverifiable: number;
77
+ /** Forged ids not already reported. */
78
+ findings: ForgedEnvelopeFinding[];
79
+ }
80
+ /**
81
+ * Full point-in-time scan of one kiro instance: read its transcript, extract
82
+ * envelope candidates, verify each against the outbox. `alreadyReportedIds`
83
+ * suppresses repeat findings; the caller persists newly reported ids.
84
+ *
85
+ * Fail-closed (#1007): an unreadable store is `unreadable-store`, a missing
86
+ * outbox with candidates is `partially-unverified` — never `clean`. Only a
87
+ * successfully read transcript with every candidate verified is clean.
88
+ */
89
+ export declare function scanKiroInstanceForForgedEnvelopes(opts: {
90
+ instanceName: string;
91
+ workingDirectory: string;
92
+ outboxDbPath: string;
93
+ knownInstances: Set<string>;
94
+ /** Explicit store DB (tests/overrides). Otherwise resolved from storeHome. */
95
+ kiroDbPath?: string;
96
+ /**
97
+ * Credential-profile store home, exactly as the transcript factory resolves
98
+ * it per instance — a profiled instance keeps its transcript in its own
99
+ * data.sqlite3, not the shared store.
100
+ */
101
+ storeHome?: string;
102
+ alreadyReportedIds?: Set<string>;
103
+ }): KiroForgedScanResult;
104
+ export interface ScanTargetConfig {
105
+ name: string;
106
+ backend: string;
107
+ workingDirectory?: string;
108
+ backendOptions?: Record<string, Record<string, unknown>>;
109
+ }
110
+ /**
111
+ * Effective per-instance scan config, merged EXACTLY like the daemon's
112
+ * (`loadFleetConfig`: fleet defaults deep-merged under instance overrides).
113
+ * The credential profile most often lives in defaults — resolving it from the
114
+ * raw instance block alone scans the wrong store (#1007 round-3).
115
+ */
116
+ export declare function resolveScanTarget(name: string, rawInstance: Partial<InstanceConfig> | undefined, fleetDefaults: Partial<InstanceConfig>): ScanTargetConfig;
117
+ export interface ScanCommandFinding {
118
+ from: string;
119
+ message_id: string;
120
+ excerpt: string;
121
+ suggested_injection: string;
122
+ }
123
+ export interface ScanCommandRow {
124
+ instance: string;
125
+ backend: string;
126
+ status: string;
127
+ reason?: string;
128
+ note?: string;
129
+ conversation?: string | null;
130
+ checked?: number;
131
+ delivered?: number;
132
+ unverifiable?: number;
133
+ findings: ScanCommandFinding[];
134
+ }
135
+ export interface ScanCommandResult {
136
+ rows: ScanCommandRow[];
137
+ newFindings: number;
138
+ unreadable: number;
139
+ /** no-conversation, partially-unverified, or missing working_directory. */
140
+ incomplete: number;
141
+ /**
142
+ * Fail-closed exit codes: 2 forged, 3 unreadable store, 4 incomplete
143
+ * verification, 0 only when every scanned transcript verified clean.
144
+ */
145
+ exitCode: number;
146
+ }
147
+ /**
148
+ * Run the scan over resolved targets. Pure orchestration over
149
+ * {@link scanKiroInstanceForForgedEnvelopes} — the CLI only formats and exits.
150
+ */
151
+ export declare function executeForgedEnvelopeScan(opts: {
152
+ targets: ScanTargetConfig[];
153
+ knownInstances: Set<string>;
154
+ outboxDbPath: string;
155
+ dataDir: string;
156
+ kiroDbOverride?: string;
157
+ alreadyReportedIds?: Set<string>;
158
+ }): ScanCommandResult;
159
+ export declare function loadReportedEnvelopeIds(dataDir: string): Set<string>;
160
+ export declare function recordReportedEnvelopeIds(dataDir: string, ids: Iterable<string>): void;
@@ -0,0 +1,309 @@
1
+ /**
2
+ * #995 (#856-B): post-hoc detection of fabricated peer envelopes.
3
+ *
4
+ * (A) lets an agent verify a peer message before acting; (C) instruments the
5
+ * transport. This module is the defense-in-depth fallback: scan a receiver's
6
+ * OWN kiro
7
+ * transcript for fleet-envelope shapes (`[from:<instance>] … (message_id:
8
+ * xmsg-…)`) and check each message_id against the durable delivery record
9
+ * (#856's `delivery_status` message_id selector). An id nobody delivered is
10
+ * not a transport glitch — the receiver's model fabricated the turn.
11
+ *
12
+ * Only kiro has a reader here: its conversation store
13
+ * (`~/.local/share/kiro-cli/data.sqlite3`, conversations_v2) is on disk.
14
+ * Claude jsonl / Codex rollouts / other backends are per-backend follow-ups.
15
+ *
16
+ * Everything query-side is read-only: the kiro store opens readonly and the
17
+ * outbox is read through `DeliveryOutbox.queryStatusReadOnly` (operator
18
+ * semantics — existence only, content stays redacted).
19
+ */
20
+ import { t } from "./locale.js";
21
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
22
+ import { join } from "node:path";
23
+ import { credentialStoreHomeForInstance } from "./backend/credential-profile.js";
24
+ import { deepMergeGeneric, getEffectiveInstanceDefaults } from "./config.js";
25
+ import { DeliveryOutbox, deliveryStatusSelector } from "./delivery-outbox.js";
26
+ import { extractKiroAssistantStrings, kiroStoreDbPath, readKiroConversationStatus } from "./transcript-sources.js";
27
+ const FROM_TAG_RE = /\[from:([^\]\n]{1,160})\]/g;
28
+ // No closing paren required: the daemon renders metadata pipe-separated in
29
+ // one pair — `(message_id: … | correlation_id: …)` — so the id is usually
30
+ // followed by ` |`, not `)`.
31
+ const MESSAGE_ID_RE = /\(message_id:\s*(xmsg-[A-Za-z0-9_-]{1,64})/g;
32
+ /** A pasted envelope keeps header and metadata within one screen of text. */
33
+ const MAX_ENVELOPE_SPAN = 8000;
34
+ /**
35
+ * A `[from:…]` tag only counts when it names a real instance — either the
36
+ * bare name or the daemon's `Display (name)` render. Anything else is a
37
+ * quote, a doc snippet, or a name the model invented along with the turn.
38
+ */
39
+ function resolveInstanceName(raw, knownInstances) {
40
+ const name = raw.trim();
41
+ if (knownInstances.has(name))
42
+ return name;
43
+ const wrapped = /^(.+?)\s*\(([^()]+)\)$/.exec(name);
44
+ if (wrapped && knownInstances.has(wrapped[2].trim()))
45
+ return wrapped[2].trim();
46
+ return null;
47
+ }
48
+ /**
49
+ * Find fleet-envelope candidates in one assistant text. Each message_id is
50
+ * paired with the nearest `[from:]` tag naming a real instance within
51
+ * MAX_ENVELOPE_SPAN characters (preceding preferred); pairs are deduped.
52
+ */
53
+ export function extractEnvelopeCandidates(text, knownInstances) {
54
+ const tags = [];
55
+ FROM_TAG_RE.lastIndex = 0;
56
+ let tag;
57
+ while ((tag = FROM_TAG_RE.exec(text)) !== null) {
58
+ const instance = resolveInstanceName(tag[1], knownInstances);
59
+ if (instance)
60
+ tags.push({ index: tag.index, instance });
61
+ }
62
+ if (tags.length === 0)
63
+ return [];
64
+ const out = [];
65
+ const seen = new Set();
66
+ MESSAGE_ID_RE.lastIndex = 0;
67
+ let mid;
68
+ while ((mid = MESSAGE_ID_RE.exec(text)) !== null) {
69
+ let best = null;
70
+ for (const t of tags) {
71
+ const dist = Math.abs(t.index - mid.index);
72
+ if (dist > MAX_ENVELOPE_SPAN)
73
+ continue;
74
+ if (!best) {
75
+ best = t;
76
+ continue;
77
+ }
78
+ const bestDist = Math.abs(best.index - mid.index);
79
+ // Prefer the preceding tag; on the same side prefer the nearer one.
80
+ const tPre = t.index <= mid.index;
81
+ const bPre = best.index <= mid.index;
82
+ if ((tPre && !bPre) || (tPre === bPre && dist < bestDist))
83
+ best = t;
84
+ }
85
+ if (!best)
86
+ continue;
87
+ const key = `${best.instance}|${mid[1]}`;
88
+ if (seen.has(key))
89
+ continue;
90
+ seen.add(key);
91
+ const start = Math.max(0, Math.min(best.index, mid.index) - 120);
92
+ out.push({
93
+ fromInstance: best.instance,
94
+ messageId: mid[1],
95
+ excerpt: text.slice(start, mid.index + mid[0].length).replace(/\s+/g, " ").slice(0, 300),
96
+ });
97
+ }
98
+ return out;
99
+ }
100
+ /**
101
+ * Assistant texts the kiro store holds for one instance conversation.
102
+ * Discriminated (#1007): a missing/unreadable store is NOT an empty
103
+ * transcript — the caller must not report it as clean.
104
+ */
105
+ export function readKiroAssistantTexts(workingDirectory, kiroDbPath = kiroStoreDbPath()) {
106
+ const read = readKiroConversationStatus(kiroDbPath, workingDirectory, true);
107
+ if (read.status === "no-row")
108
+ return { kind: "no-conversation" };
109
+ if (read.status === "error")
110
+ return { kind: "unreadable", reason: read.reason };
111
+ const history = read.conversation.history;
112
+ if (!history)
113
+ return { kind: "unreadable", reason: "conversation history is missing or unparseable" };
114
+ const texts = [];
115
+ for (const entry of history)
116
+ texts.push(...extractKiroAssistantStrings(entry));
117
+ return { kind: "ok", conversationId: read.conversation.conversationId, texts };
118
+ }
119
+ /**
120
+ * Verify candidates against the durable record — the same oracle (A) agents
121
+ * use before destructive ops. Missing outbox DB (or an unreadable one) is
122
+ * `unverifiable`, never forged: absence of evidence is not fabrication.
123
+ */
124
+ export function verifyEnvelopeCandidates(outboxDbPath, messageIds) {
125
+ const verdicts = new Map();
126
+ if (!existsSync(outboxDbPath)) {
127
+ for (const id of messageIds)
128
+ verdicts.set(id, "unverifiable");
129
+ return verdicts;
130
+ }
131
+ for (const id of new Set(messageIds)) {
132
+ try {
133
+ const page = DeliveryOutbox.queryStatusReadOnly(outboxDbPath, deliveryStatusSelector({ message_id: id }));
134
+ verdicts.set(id, page.items.length > 0 ? "delivered" : "forged");
135
+ }
136
+ catch {
137
+ verdicts.set(id, "unverifiable");
138
+ }
139
+ }
140
+ return verdicts;
141
+ }
142
+ /**
143
+ * The exact correction to inject into the instance (send_to_instance or a
144
+ * mid-turn steer): the message it acted on was never fleet-delivered.
145
+ */
146
+ export function formatForgedEnvelopeWarning(instanceName, finding) {
147
+ return t("delivery.forged_warning", finding.fromInstance, finding.messageId);
148
+ }
149
+ /**
150
+ * Full point-in-time scan of one kiro instance: read its transcript, extract
151
+ * envelope candidates, verify each against the outbox. `alreadyReportedIds`
152
+ * suppresses repeat findings; the caller persists newly reported ids.
153
+ *
154
+ * Fail-closed (#1007): an unreadable store is `unreadable-store`, a missing
155
+ * outbox with candidates is `partially-unverified` — never `clean`. Only a
156
+ * successfully read transcript with every candidate verified is clean.
157
+ */
158
+ export function scanKiroInstanceForForgedEnvelopes(opts) {
159
+ const base = {
160
+ instanceName: opts.instanceName, conversationId: null, checked: 0, delivered: 0, unverifiable: 0, findings: [],
161
+ };
162
+ const kiroDbPath = opts.kiroDbPath ?? kiroStoreDbPath(opts.storeHome);
163
+ const convo = readKiroAssistantTexts(opts.workingDirectory, kiroDbPath);
164
+ if (convo.kind === "unreadable") {
165
+ return { ...base, status: "unreadable-store", reason: convo.reason };
166
+ }
167
+ if (convo.kind === "no-conversation") {
168
+ return { ...base, status: "no-conversation" };
169
+ }
170
+ const candidates = [];
171
+ for (const text of convo.texts)
172
+ candidates.push(...extractEnvelopeCandidates(text, opts.knownInstances));
173
+ const verdicts = verifyEnvelopeCandidates(opts.outboxDbPath, candidates.map(c => c.messageId));
174
+ const reported = opts.alreadyReportedIds ?? new Set();
175
+ const result = {
176
+ ...base, status: "clean", conversationId: convo.conversationId, checked: candidates.length,
177
+ };
178
+ const seenFinding = new Set();
179
+ for (const c of candidates) {
180
+ const verdict = verdicts.get(c.messageId);
181
+ if (verdict === "delivered")
182
+ result.delivered++;
183
+ else if (verdict === "unverifiable")
184
+ result.unverifiable++;
185
+ else if (!reported.has(c.messageId) && !seenFinding.has(c.messageId)) {
186
+ seenFinding.add(c.messageId);
187
+ result.findings.push({ ...c, warning: formatForgedEnvelopeWarning(opts.instanceName, c) });
188
+ }
189
+ }
190
+ if (result.findings.length > 0)
191
+ result.status = "forged-envelopes";
192
+ else if (result.unverifiable > 0)
193
+ result.status = "partially-unverified";
194
+ return result;
195
+ }
196
+ /**
197
+ * Effective per-instance scan config, merged EXACTLY like the daemon's
198
+ * (`loadFleetConfig`: fleet defaults deep-merged under instance overrides).
199
+ * The credential profile most often lives in defaults — resolving it from the
200
+ * raw instance block alone scans the wrong store (#1007 round-3).
201
+ */
202
+ export function resolveScanTarget(name, rawInstance, fleetDefaults) {
203
+ const merged = deepMergeGeneric(getEffectiveInstanceDefaults(fleetDefaults), rawInstance ?? {});
204
+ const backend = merged.backend ?? "claude-code";
205
+ return {
206
+ name,
207
+ backend,
208
+ workingDirectory: merged.working_directory,
209
+ backendOptions: merged.backend_options,
210
+ };
211
+ }
212
+ /**
213
+ * Run the scan over resolved targets. Pure orchestration over
214
+ * {@link scanKiroInstanceForForgedEnvelopes} — the CLI only formats and exits.
215
+ */
216
+ export function executeForgedEnvelopeScan(opts) {
217
+ const reported = opts.alreadyReportedIds ?? new Set();
218
+ let newFindings = 0;
219
+ let unreadable = 0;
220
+ let incomplete = 0;
221
+ const rows = [];
222
+ for (const target of opts.targets) {
223
+ if (target.backend !== "kiro-cli") {
224
+ // Other backends keep their transcripts elsewhere (Claude jsonl, Codex
225
+ // rollouts, …) — per-backend follow-ups, not silent skips.
226
+ rows.push({
227
+ instance: target.name,
228
+ backend: target.backend,
229
+ status: "unsupported-backend",
230
+ note: "transcript source is a per-backend follow-up (#995)",
231
+ findings: [],
232
+ });
233
+ continue;
234
+ }
235
+ if (!target.workingDirectory) {
236
+ incomplete++;
237
+ rows.push({
238
+ instance: target.name,
239
+ backend: target.backend,
240
+ status: "error",
241
+ note: "fleet config has no working_directory for this instance",
242
+ findings: [],
243
+ });
244
+ continue;
245
+ }
246
+ // A profiled instance writes its transcript to its own store, not the
247
+ // shared one — scan the store its CLI actually uses.
248
+ const storeHome = opts.kiroDbOverride
249
+ ? undefined
250
+ : credentialStoreHomeForInstance(opts.dataDir, target.backend, target.backendOptions?.[target.backend]);
251
+ const result = scanKiroInstanceForForgedEnvelopes({
252
+ instanceName: target.name,
253
+ workingDirectory: target.workingDirectory,
254
+ outboxDbPath: opts.outboxDbPath,
255
+ knownInstances: opts.knownInstances,
256
+ ...(opts.kiroDbOverride ? { kiroDbPath: opts.kiroDbOverride } : {}),
257
+ ...(storeHome ? { storeHome } : {}),
258
+ alreadyReportedIds: reported,
259
+ });
260
+ for (const f of result.findings)
261
+ reported.add(f.messageId);
262
+ newFindings += result.findings.length;
263
+ if (result.status === "unreadable-store")
264
+ unreadable++;
265
+ if (result.status === "no-conversation" || result.status === "partially-unverified")
266
+ incomplete++;
267
+ rows.push({
268
+ instance: target.name,
269
+ backend: target.backend,
270
+ status: result.status,
271
+ ...(result.reason ? { reason: result.reason } : {}),
272
+ conversation: result.conversationId,
273
+ checked: result.checked,
274
+ delivered: result.delivered,
275
+ unverifiable: result.unverifiable,
276
+ findings: result.findings.map(f => ({
277
+ from: f.fromInstance,
278
+ message_id: f.messageId,
279
+ excerpt: f.excerpt,
280
+ // Inject this text into the instance (send_to_instance or a
281
+ // mid-turn steer): the CLI cannot paste into a live pane itself.
282
+ suggested_injection: f.warning,
283
+ })),
284
+ });
285
+ }
286
+ const exitCode = newFindings > 0 ? 2 : unreadable > 0 ? 3 : incomplete > 0 ? 4 : 0;
287
+ return { rows, newFindings, unreadable, incomplete, exitCode };
288
+ }
289
+ /* ------------------------------------------------------- report dedup state */
290
+ export function loadReportedEnvelopeIds(dataDir) {
291
+ try {
292
+ const raw = JSON.parse(readFileSync(join(dataDir, "forged-envelope-reports.json"), "utf-8"));
293
+ if (!Array.isArray(raw.reported_message_ids))
294
+ return new Set();
295
+ return new Set(raw.reported_message_ids.filter((id) => typeof id === "string"));
296
+ }
297
+ catch {
298
+ return new Set();
299
+ }
300
+ }
301
+ export function recordReportedEnvelopeIds(dataDir, ids) {
302
+ const merged = loadReportedEnvelopeIds(dataDir);
303
+ for (const id of ids)
304
+ merged.add(id);
305
+ mkdirSync(dataDir, { recursive: true });
306
+ const path = join(dataDir, "forged-envelope-reports.json");
307
+ writeFileSync(path, JSON.stringify({ reported_message_ids: [...merged] }, null, 2), { encoding: "utf8", mode: 0o600 });
308
+ }
309
+ //# sourceMappingURL=forged-envelope-scan.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"forged-envelope-scan.js","sourceRoot":"","sources":["../src/forged-envelope-scan.ts"],"names":[],"mappings":"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"}
@@ -45,8 +45,11 @@ persona emoji replaces your `delivered` stamp with one of your own, the way
45
45
  Don't retry the same value.
46
46
 
47
47
  Only change your own `delivered` stamp unless someone asked for more. The
48
- other statuses are how people see a message is still in progress, and
49
- `progress_prefix` is the emoji leading your progress messages.
48
+ other statuses are how people see a message is still in progress,
49
+ `progress_prefix` is the emoji leading your progress messages, and `photo` /
50
+ `attachment` are the stamps on a photo / file your bot saved (default 📸 / 📎;
51
+ 👌 / 👍 on Telegram, which only takes its fixed reaction set — the same check
52
+ applies to them).
50
53
 
51
54
  ClassicBot instances have no per-instance stamps. The tool says so, and the
52
55
  connection's emojis are set by an operator in Settings.
@@ -6,6 +6,7 @@ import type { EventLog } from "./event-log.js";
6
6
  import type { TmuxControlClient } from "./tmux-control.js";
7
7
  import type { FleetInstructionsParams } from "./instructions.js";
8
8
  import type { DeliveryStatus } from "./status-emojis.js";
9
+ import { type PauseReason } from "./pause-marker.js";
9
10
  import { type ProviderUsage } from "./usage/providers.js";
10
11
  import type { SpawnGate } from "./spawn-gate.js";
11
12
  import type { StormWindow } from "./storm-window.js";
@@ -16,6 +17,15 @@ export { isFleetStartCommandLine } from "./fleet-lock.js";
16
17
  export interface BackendInstallationInfo {
17
18
  binary: string;
18
19
  install: string;
20
+ /**
21
+ * #1092: where this backend's own installer puts the binary, read from the
22
+ * installer script itself. /install-cli verifies on a fresh login shell
23
+ * first, but an installer may record its PATH only in a file a login shell
24
+ * never runs: codex's writes `~/.bashrc` when `$SHELL` is bash, below the
25
+ * stock early `return` for non-interactive shells. Checked only when the
26
+ * login shell cannot find the binary.
27
+ */
28
+ binDirs?: (env: NodeJS.ProcessEnv, home: string) => string[];
19
29
  }
20
30
  /** Shared CLI metadata used by startup validation and ClassicBot onboarding. */
21
31
  export declare const BACKEND_INSTALLATION_INFO: Readonly<Record<string, BackendInstallationInfo>>;
@@ -113,8 +123,32 @@ export interface LifecycleContext {
113
123
  finishDeliveryStatus?(instanceName: string, chatId: string, messageId: string, status: DeliveryStatus, threadId?: string): void;
114
124
  /** #1005: the instance's own status emojis, for the instructions' "avoid these" line. */
115
125
  statusEmojiAvoidList?(instanceName: string): string[];
116
- startPersistedPausedInstance(name: string): Promise<void>;
126
+ startPersistedPausedInstance(name: string, transition?: TransitionHandle): Promise<void>;
127
+ /** The restart already in flight for `name`, if any — an explicit wake joins it (Phase 2a). */
128
+ restartInFlight?(name: string): Promise<void> | undefined;
129
+ /** Phase 2b: a wake that the wake coordinator did not start (operator, user message) succeeded. */
130
+ onExternalWake?(name: string): void;
117
131
  }
132
+ /**
133
+ * Proof that the caller is running inside `name`'s current transition. Only
134
+ * direct internal calls hand it on (restart → stop → start, wake → marker-only
135
+ * start); a detached callback — an event handler, timer or setImmediate fired
136
+ * while a transition is still awaiting — has none and queues behind it.
137
+ */
138
+ export interface TransitionHandle {
139
+ readonly name: string;
140
+ readonly token: symbol;
141
+ }
142
+ /** Thrown by a start whose generation was invalidated (stop/restart) before it could publish. */
143
+ export declare class SupersededStartError extends Error {
144
+ constructor(name: string);
145
+ }
146
+ /**
147
+ * How long a target just woken for queued work is held from idle auto-pause
148
+ * before its first queued delivery is claimed (Phase 2 design §1.6): the
149
+ * cross-instance idle gate (60s) plus a 60s grace.
150
+ */
151
+ export declare const PRECLAIM_LEASE_MS = 120000;
118
152
  /**
119
153
  * Working directories of the fleet's other instances on `backendName` (#984):
120
154
  * Codex consults them only to decide whether `resume --last` could take a
@@ -124,7 +158,10 @@ export declare function peerWorkingDirectories(fleetConfig: FleetConfig | null,
124
158
  /** What attachIncidentHandlers needs from a Daemon — the real one satisfies it. */
125
159
  export interface IncidentEventSource {
126
160
  on(event: string, handler: (...args: any[]) => void): unknown;
127
- requestPauseWhenIdle(): void;
161
+ requestPauseWhenIdle(opts?: {
162
+ reconfirmAuth?: boolean;
163
+ reason?: PauseReason;
164
+ }): void;
128
165
  /** Re-checks that a Codex pane is live after an async quota probe. */
129
166
  isCodexLivePane?(): Promise<boolean>;
130
167
  /** Existing occurrence count used to preserve Codex capacity history on restart. */
@@ -133,6 +170,9 @@ export interface IncidentEventSource {
133
170
  getHangDetector?(): {
134
171
  on(event: string, handler: (...args: any[]) => void): unknown;
135
172
  } | null;
173
+ /** Real daemons buffer `pty_error` raised by the startup scan until the lifecycle can handle it. */
174
+ holdStartupIncidents?(): void;
175
+ releaseStartupIncidents?(): void;
136
176
  }
137
177
  /** Arguments accepted by handleCreate — mirrors CreateInstanceArgs in outbound-schemas.ts
138
178
  * plus internal-only fields forwarded by deploy_template (profile-derived). */
@@ -193,6 +233,21 @@ export declare class InstanceLifecycle {
193
233
  private ctx;
194
234
  /** Active daemon processes: instanceName → Daemon */
195
235
  readonly daemons: Map<string, import("./daemon.js").Daemon>;
236
+ /**
237
+ * Phase 2a: one transition (start / stop / wake / restart) at a time per
238
+ * instance. A call that passes the current transition's handle runs inline;
239
+ * anything else queues.
240
+ */
241
+ private transitionTails;
242
+ private activeTransition;
243
+ /** Bumped synchronously by stop/restart: a start begun under an older epoch must not publish. */
244
+ private lifecycleEpochs;
245
+ /** Single wake flight per instance: concurrent wakes share it. */
246
+ private wakeFlights;
247
+ /** Starts that begin their idle window now instead of at the persisted last inbound. */
248
+ private activitySeedNow;
249
+ /** When each instance was last woken or started for work — the pre-claim lease clock. */
250
+ private wokeAt;
196
251
  /** backend → last auth-error alert time, so one expiry sends one alert. */
197
252
  private lastAuthAlertAt;
198
253
  /**
@@ -325,11 +380,39 @@ export declare class InstanceLifecycle {
325
380
  */
326
381
  attachDeliveryStatusHandlers(name: string, daemon: IncidentEventSource): void;
327
382
  attachIncidentHandlers(name: string, daemon: IncidentEventSource): void;
328
- start(name: string, config: InstanceConfig, topicMode: boolean, runtimeIdentity?: FleetInstructionsParams["runtimeIdentity"]): Promise<void>;
383
+ epochOf(name: string): number;
384
+ /** Invalidate every start/wake already running for `name` (they will not publish). */
385
+ invalidate(name: string): void;
386
+ /** Whether `transition` is `name`'s transition and is the one running now. */
387
+ holds(name: string, transition: TransitionHandle | undefined): transition is TransitionHandle;
388
+ /** Run `fn` as `name`'s next transition, or inline when `parent` is the transition running now. */
389
+ runTransition<T>(name: string, fn: (transition: TransitionHandle) => Promise<T>, parent?: TransitionHandle): Promise<T>;
390
+ /** The next start of `name` is a wake/restart/explicit start: begin its idle window now. */
391
+ markActivitySeedNow(name: string): void;
392
+ /** Drop a seed request that no start consumed (the start returned early or failed before it). */
393
+ clearActivitySeedNow(name: string): void;
394
+ /** Record that `name` was just woken or (re)started, opening its pre-claim lease window. */
395
+ noteWokeForWork(name: string, now?: number): void;
396
+ /**
397
+ * Phase 2a work lease (design §1.6). Held while a durable submission to the
398
+ * target is in flight (unbounded: no timer releases a writer), or for
399
+ * PRECLAIM_LEASE_MS after a wake/start while queued work is still waiting to
400
+ * be claimed. It holds only the idle-timeout pause and warm-cap eviction —
401
+ * never an auth-deferred or operator pause.
402
+ */
403
+ hasWorkLease(name: string, now?: number): boolean;
404
+ start(name: string, config: InstanceConfig, topicMode: boolean, runtimeIdentity?: FleetInstructionsParams["runtimeIdentity"], transition?: TransitionHandle): Promise<void>;
405
+ private startInTransition;
329
406
  isPaused(name: string): boolean;
330
407
  getLastPausedAt(name: string): number | null;
331
- pause(name: string): Promise<void>;
332
- wake(name: string, timeoutMs?: number): Promise<void>;
408
+ pause(name: string, reason?: PauseReason): Promise<void>;
409
+ /** When `name` was last woken or (re)started for work, if recorded. */
410
+ wokeAtFor(name: string): number | undefined;
411
+ wake(name: string, timeoutMs?: number, transition?: TransitionHandle, opts?: {
412
+ source?: "coordinator" | "external";
413
+ }): Promise<void>;
414
+ private wakeInner;
415
+ private wakeInTransition;
333
416
  /**
334
417
  * Ownership boundary for a daemon that is not yet registered: if start()
335
418
  * rejects, nothing else will ever dispose it, so do it here before
@@ -342,7 +425,8 @@ export declare class InstanceLifecycle {
342
425
  }, name: string, logger: Logger): Promise<void>;
343
426
  /** Stop the registered daemon only if it is still `daemon` (a concurrent restart may have replaced it). */
344
427
  stopIfCurrent(name: string, daemon: unknown): Promise<boolean>;
345
- stop(name: string): Promise<void>;
428
+ stop(name: string, transition?: TransitionHandle): Promise<void>;
429
+ private stopInTransition;
346
430
  remove(name: string, authorization: ExplicitInstanceRemoval): Promise<void>;
347
431
  /** Handle create_instance tool call from a daemon. */
348
432
  handleCreate(args: LifecycleCreateArgs, respond: (result: unknown, error?: string) => void, adapterId?: string): Promise<void>;