omp-conductor 0.18.1 → 0.18.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/REFERENCE.md +1 -1
- package/agents/to-spec.md +6 -2
- package/package.json +1 -1
- package/schema/config.schema.json +6 -1
- package/src/arm-challenge.ts +250 -57
- package/src/briefs/orchestrator.md +21 -8
- package/src/briefs/to-spec.md +6 -2
- package/src/cli.ts +122 -1
- package/src/command-manifest.ts +16 -5
- package/src/commands/arm.ts +1 -1
- package/src/commands/intake.ts +4 -19
- package/src/commands/watch.ts +4 -17
- package/src/config-schema.ts +19 -6
- package/src/config.ts +23 -8
- package/src/daemon.ts +150 -241
- package/src/decisions.ts +19 -11
- package/src/doctor.ts +55 -129
- package/src/escalate.ts +22 -11
- package/src/fleet.ts +93 -218
- package/src/host.ts +7 -332
- package/src/omp-settings.ts +19 -0
- package/src/omp.ts +11 -56
- package/src/orchestrator-tick.ts +225 -37
- package/src/session-host.ts +6 -41
- package/src/setup-host.ts +27 -10
- package/src/setup-wizard.ts +141 -1
- package/src/setup.ts +54 -4
- package/src/status-render.ts +138 -1
- package/src/to-spec.ts +23 -2
- package/src/types.ts +62 -6
- package/src/verbs/server.ts +38 -3
- package/src/worker.ts +0 -13
package/src/doctor.ts
CHANGED
|
@@ -46,9 +46,11 @@ import {
|
|
|
46
46
|
dbBackupDirFor,
|
|
47
47
|
findProject,
|
|
48
48
|
loadConfig,
|
|
49
|
+
resolveArmProof,
|
|
49
50
|
resolveCaps,
|
|
50
51
|
stateDir,
|
|
51
52
|
} from "./config.ts";
|
|
53
|
+
import { observeArmChallenge } from "./arm-challenge.ts";
|
|
52
54
|
import { ompSettingsOverlay, sessionRootDir } from "./omp-settings.ts";
|
|
53
55
|
import {
|
|
54
56
|
DEFAULT_HERDR_UNIT,
|
|
@@ -85,10 +87,7 @@ import {
|
|
|
85
87
|
SYSTEMD_UNIT_DIR,
|
|
86
88
|
tickCwdForProject,
|
|
87
89
|
totalConfiguredWorkers,
|
|
88
|
-
workerAclHealth,
|
|
89
|
-
type WorkerAclHealth,
|
|
90
90
|
} from "./setup-host.ts";
|
|
91
|
-
import { WORKER_ACCOUNT } from "./host.ts";
|
|
92
91
|
import { checkTokenScopes, type ScopeCheck } from "./setup.ts";
|
|
93
92
|
import { DB_SNAPSHOT_STEM, dbPath, LIVE_STATES } from "./store.ts";
|
|
94
93
|
import { telegramReportSend, type ReportSend } from "./reports.ts";
|
|
@@ -101,7 +100,7 @@ import {
|
|
|
101
100
|
type HerdrAgentList,
|
|
102
101
|
} from "./orchestrator-tick.ts";
|
|
103
102
|
import type { ConductorConfig, ProjectConfig, RepoTarget, RunState } from "./types.ts";
|
|
104
|
-
|
|
103
|
+
import { DEFAULT_ARM_PROOF, type ArmProof } from "./types.ts";
|
|
105
104
|
/**
|
|
106
105
|
* `doctor`'s finding vocabulary. `pass`/`warn`/`fail` — a warning renders the
|
|
107
106
|
* same but never flips the exit code; exit is nonzero iff any finding fails.
|
|
@@ -256,8 +255,10 @@ export interface DoctorDeps {
|
|
|
256
255
|
/** The topic id an arm challenge would send into for this project —
|
|
257
256
|
* resolveProjectTopicId's pin-or-live-claim; undefined means flat chat. */
|
|
258
257
|
armSendTopic?: (project: ProjectConfig) => number | undefined;
|
|
259
|
-
/** The session
|
|
260
|
-
* project — tick-cwd-derived plus the live claim's own directory.
|
|
258
|
+
/** The session surface the claim-only plumbing verdict judges for this
|
|
259
|
+
* project — tick-cwd-derived plus the live claim's own directory. The
|
|
260
|
+
* challenge proof reads conductor state instead (#614), so this feeds
|
|
261
|
+
* claim-only verdict checks only. */
|
|
261
262
|
armScanDirs?: (project: ProjectConfig) => readonly string[];
|
|
262
263
|
/** Whether a recorded claim or dm-owner pid is live, with omp-telegram's
|
|
263
264
|
* topics.ts semantics (EPERM is dead). */
|
|
@@ -275,13 +276,6 @@ export interface DoctorDeps {
|
|
|
275
276
|
tickAgentName?: (project: ProjectConfig) => string | undefined;
|
|
276
277
|
/** Clock, so a run is deterministic in tests. */
|
|
277
278
|
now?: () => number;
|
|
278
|
-
/** The linked worker config paths' effective-ACL verdict for the dedicated
|
|
279
|
-
* worker account (#835): `checkable` paths judged, `missing` the ones that
|
|
280
|
-
* do not currently grant the worker's needed effective access. Read-only
|
|
281
|
-
* and injectable so no test ever touches the host's ACLs; the production
|
|
282
|
-
* wiring is the identity plan's own getfacl probe, so `doctor` and
|
|
283
|
-
* `setup host` cannot disagree about an ACL. */
|
|
284
|
-
workerAclHealth?: () => WorkerAclHealth;
|
|
285
279
|
/** The one opt-in side effect: send one self-identified Telegram probe. */
|
|
286
280
|
probeTelegram?: boolean;
|
|
287
281
|
}
|
|
@@ -762,6 +756,12 @@ function unresolvableShell(unit: string): boolean {
|
|
|
762
756
|
* state resolved at staging time, so a rendered value doctor cannot reproduce
|
|
763
757
|
* is excluded from both sides rather than reported as a difference — an
|
|
764
758
|
* unresolvable canonical is "cannot compare", never drift (#511).
|
|
759
|
+
*
|
|
760
|
+
* The service `PATH=` line has no such exemption: since #879 it is canonical
|
|
761
|
+
* on both sides (`defaultServiceRuntime` composes it from the resolved fleet
|
|
762
|
+
* binaries plus the standard system directories, never from the rendering
|
|
763
|
+
* process's environment), so a differing installed value is genuine drift and
|
|
764
|
+
* must fail here with the value named.
|
|
765
765
|
*/
|
|
766
766
|
export function unitDrift(installed: string, canonical: string): { differences: string[] } {
|
|
767
767
|
const want = canonical
|
|
@@ -916,36 +916,6 @@ function ownershipProbe(probes: Probes): Finding {
|
|
|
916
916
|
return failFinding("systemd-ownership", summary, fix);
|
|
917
917
|
}
|
|
918
918
|
|
|
919
|
-
/**
|
|
920
|
-
* The dedicated worker account's path ACLs (#835): every linked worker config
|
|
921
|
-
* path must grant the worker its *effective* search/read permissions. The
|
|
922
|
-
* installed OMP harness chmods its agent config dir back to 0700 on every
|
|
923
|
-
* open, and a chmod rewrites the ACL mask — the named entry survives as
|
|
924
|
-
* `user:omp-worker:--x #effective:---` while the worker's access silently
|
|
925
|
-
* vanishes, which is exactly the incident this finding exists to catch
|
|
926
|
-
* (`${INCIDENTS.workerAcl}`). The verdict comes from the identity plan's own
|
|
927
|
-
* probe through the read-only {@link DoctorDeps.workerAclHealth} seam, so a
|
|
928
|
-
* pass here is a pass the plan agrees with and a fail names the paths whose
|
|
929
|
-
* mask is stripping the grant.
|
|
930
|
-
*/
|
|
931
|
-
function workerAclProbeFinding(probes: Probes): Finding {
|
|
932
|
-
const health = probes.workerAclHealth();
|
|
933
|
-
if (health.checkable === 0) {
|
|
934
|
-
return passFinding("worker-acl", "no linked worker config paths on this host yet — nothing to check");
|
|
935
|
-
}
|
|
936
|
-
if (health.missing.length === 0) {
|
|
937
|
-
return passFinding(
|
|
938
|
-
"worker-acl",
|
|
939
|
-
`${health.checkable} linked worker config path(s) grant ${WORKER_ACCOUNT} effective search/read access`,
|
|
940
|
-
);
|
|
941
|
-
}
|
|
942
|
-
return failFinding(
|
|
943
|
-
"worker-acl",
|
|
944
|
-
`the ${WORKER_ACCOUNT} account's ACL is not effective on ${health.missing.join(", ")} — a named entry the ACL mask strips is ` +
|
|
945
|
-
"unreachable by the worker, and the next admitted worker dies on EACCES before connecting",
|
|
946
|
-
"re-run `omp-conductor setup host`: the transaction re-applies the ACL grants (mask included) after the final fleet restart",
|
|
947
|
-
);
|
|
948
|
-
}
|
|
949
919
|
|
|
950
920
|
/** IANA timezone in reporting config — an invalid zone silently mis-schedules
|
|
951
921
|
* the availability window and the daily digest (#273). */
|
|
@@ -1173,91 +1143,43 @@ function herdrAgentNameProbe(probes: Probes, p: ProjectConfig): Finding {
|
|
|
1173
1143
|
}
|
|
1174
1144
|
|
|
1175
1145
|
/**
|
|
1176
|
-
* #
|
|
1177
|
-
* scans.
|
|
1146
|
+
* #614 check — the state of this project's arming acknowledgement transaction.
|
|
1178
1147
|
*
|
|
1179
|
-
*
|
|
1180
|
-
*
|
|
1181
|
-
*
|
|
1182
|
-
*
|
|
1183
|
-
*
|
|
1184
|
-
*
|
|
1185
|
-
* is
|
|
1186
|
-
*
|
|
1148
|
+
* The challenge proof is conductor-owned state now: the host records a pending
|
|
1149
|
+
* challenge and waits for the acknowledgement the orchestrator's inbound
|
|
1150
|
+
* adapter writes. No transcript location is diagnosed any more — where (or
|
|
1151
|
+
* whether) a session file lives is not part of arming — so what is worth
|
|
1152
|
+
* reporting is a transaction left open: a challenge still pending and whether
|
|
1153
|
+
* it has been acknowledged, or an expired record nothing settled. Every shape
|
|
1154
|
+
* is inert by construction (a reply past expiry, or for a replaced id, is
|
|
1155
|
+
* refused), so this informs; only the stale leftover warns.
|
|
1187
1156
|
*/
|
|
1188
|
-
function
|
|
1189
|
-
const
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
|
|
1157
|
+
function armAckProbe(probes: Probes, p: ProjectConfig): Finding {
|
|
1158
|
+
const now = probes.now();
|
|
1159
|
+
const sighting = observeArmChallenge(p.name);
|
|
1160
|
+
if (sighting === undefined) {
|
|
1161
|
+
return passFinding("arm-ack", `[${p.name}] no pending arm challenge — the acknowledgement handshake is idle`);
|
|
1162
|
+
}
|
|
1163
|
+
const window =
|
|
1164
|
+
sighting.sentAt === undefined || sighting.expiresAt === undefined
|
|
1165
|
+
? "an unreadable record window"
|
|
1166
|
+
: `sent ${new Date(sighting.sentAt).toISOString()}, expires ${new Date(sighting.expiresAt).toISOString()}`;
|
|
1167
|
+
if (sighting.expiresAt !== undefined && now >= sighting.expiresAt) {
|
|
1197
1168
|
return warnFinding(
|
|
1198
|
-
"arm-
|
|
1199
|
-
`[${p.name}]
|
|
1200
|
-
|
|
1201
|
-
? `omp-telegram has no claim registry (threads.json in its state dir) — arm scans the tick-cwd session directory, but doctor cannot tell whether the claim names a session file elsewhere`
|
|
1202
|
-
: `omp-telegram's claim registry is unreadable (${result.problem}) — arm scans the tick-cwd session directory, but doctor cannot tell whether the claim names a session file elsewhere`),
|
|
1203
|
-
`check omp-telegram's claim registry (threads.json in its state dir) is readable and the bridge is running, then re-run doctor`,
|
|
1169
|
+
"arm-ack",
|
|
1170
|
+
`[${p.name}] an arming challenge from ${window} expired without being settled — it is inert (replies past expiry are refused), but it lingers until this project's next arm replaces it`,
|
|
1171
|
+
`no action needed; the next \`arm\` for this project settles it — re-run doctor afterwards to confirm`,
|
|
1204
1172
|
);
|
|
1205
1173
|
}
|
|
1206
|
-
if (
|
|
1174
|
+
if (sighting.acknowledgedAt !== undefined) {
|
|
1207
1175
|
return passFinding(
|
|
1208
|
-
"arm-
|
|
1209
|
-
`[${p.name}]
|
|
1210
|
-
);
|
|
1211
|
-
}
|
|
1212
|
-
const claims = result.claims;
|
|
1213
|
-
const match = resolveProjectClaim(claims, p.name);
|
|
1214
|
-
const scanDir = sessionDirForCwd(tickCwdForProject(p));
|
|
1215
|
-
if (match.kind === "ambiguous") {
|
|
1216
|
-
// Several live claims answer to the project, so the claim's session file
|
|
1217
|
-
// cannot be told from a sibling pane's. Arm scans the tick-cwd directory
|
|
1218
|
-
// either way, but certifying that fallback as the scan is the miss this
|
|
1219
|
-
// probe exists to surface: the resumed transcript may live under another
|
|
1220
|
-
// claim while doctor reads the fallback as the whole story (#626).
|
|
1221
|
-
return warnFinding(
|
|
1222
|
-
"arm-session-dir",
|
|
1223
|
-
`[${p.name}] several live claims answer to this project (topics ${match.claimants.map((c) => c.threadId).join(", ")}), so the claim's session file is a coin toss — arm scans the tick-cwd session directory, but the resumed transcript may live elsewhere`,
|
|
1224
|
-
`make exactly one live claim answer to this project: rename the other panes' herdr spaces or re-claim them one at a time, then re-run doctor — or start the orchestrator pane's session from the tick cwd ${scanDir}`,
|
|
1225
|
-
);
|
|
1226
|
-
}
|
|
1227
|
-
const claim = match.kind === "match" ? match.claim : undefined;
|
|
1228
|
-
if (claim === undefined) {
|
|
1229
|
-
return passFinding(
|
|
1230
|
-
"arm-session-dir",
|
|
1231
|
-
`[${p.name}] no live omp-telegram claim answers to this project — arm scans the tick-cwd session directory`,
|
|
1232
|
-
);
|
|
1233
|
-
}
|
|
1234
|
-
if (claim.sessionFile === undefined) {
|
|
1235
|
-
return passFinding(
|
|
1236
|
-
"arm-session-dir",
|
|
1237
|
-
`[${p.name}] the live claim names no session file — arm scans the tick-cwd session directory`,
|
|
1238
|
-
);
|
|
1239
|
-
}
|
|
1240
|
-
const root = sessionsRoot();
|
|
1241
|
-
const claimDir = dirname(claim.sessionFile);
|
|
1242
|
-
if (claimDir !== root && !claimDir.startsWith(join(root, sep))) {
|
|
1243
|
-
return failFinding(
|
|
1244
|
-
"arm-session-dir",
|
|
1245
|
-
`[${p.name}] the orchestrator's claimed session file ${claim.sessionFile} is outside the session tree arm scans (${root}) — ` +
|
|
1246
|
-
`arm cannot arm while the reply can only land there`,
|
|
1247
|
-
`start the orchestrator pane's session under ${root}, or from the tick cwd ${scanDir}`,
|
|
1248
|
-
);
|
|
1249
|
-
}
|
|
1250
|
-
if (claimDir !== scanDir) {
|
|
1251
|
-
return warnFinding(
|
|
1252
|
-
"arm-session-dir",
|
|
1253
|
-
`[${p.name}] the orchestrator's claimed session file ${claim.sessionFile} lives in ${claimDir}, not the tick-cwd session directory ${scanDir} — ` +
|
|
1254
|
-
`arm follows the claim, so arming works, but this is the resumed-pane shape #600 names`,
|
|
1255
|
-
`optional tidy-up: start the pane's session from the tick cwd so transcript and cwd agree again`,
|
|
1176
|
+
"arm-ack",
|
|
1177
|
+
`[${p.name}] an arm challenge is pending (${window}) and was acknowledged ${new Date(sighting.acknowledgedAt).toISOString()} — the host consumes it on settle`,
|
|
1256
1178
|
);
|
|
1257
1179
|
}
|
|
1258
1180
|
return passFinding(
|
|
1259
|
-
"arm-
|
|
1260
|
-
`[${p.name}]
|
|
1181
|
+
"arm-ack",
|
|
1182
|
+
`[${p.name}] an arm challenge is pending (${window}), not yet acknowledged — live if \`arm\` is running right now, otherwise its window simply runs out`,
|
|
1261
1183
|
);
|
|
1262
1184
|
}
|
|
1263
1185
|
|
|
@@ -1379,9 +1301,20 @@ function telegramPlumbingProbe(probes: Probes, p: ProjectConfig): Finding {
|
|
|
1379
1301
|
`[${p.name}] outbound-only paging — no tick accessFile to arm, so no live claim, DM owner or poll ownership is required`,
|
|
1380
1302
|
);
|
|
1381
1303
|
}
|
|
1304
|
+
// Transcript-surface identity checks belong to the claim-only proof alone:
|
|
1305
|
+
// since #614 the challenge proof reads conductor acknowledgement state, so
|
|
1306
|
+
// judging it by where a transcript lives could report arming impossible
|
|
1307
|
+
// while `arm` succeeds. An unreadable config keeps the default proof —
|
|
1308
|
+
// fail-safe to the stricter verdict, exactly like armTicks.
|
|
1309
|
+
let proof: ArmProof = DEFAULT_ARM_PROOF;
|
|
1310
|
+
try {
|
|
1311
|
+
proof = resolveArmProof(p);
|
|
1312
|
+
} catch {
|
|
1313
|
+
/* unreadable config — keep today's stricter default */
|
|
1314
|
+
}
|
|
1382
1315
|
const verdict = telegramPlumbingVerdict(
|
|
1383
1316
|
probes.armSendTopic(p),
|
|
1384
|
-
{ dirs: probes.armScanDirs(p) },
|
|
1317
|
+
proof === "claim-only" ? { dirs: probes.armScanDirs(p) } : undefined,
|
|
1385
1318
|
{
|
|
1386
1319
|
channel: probes.channelState(p),
|
|
1387
1320
|
registry: probes.claimedTopics(),
|
|
@@ -1663,7 +1596,7 @@ export async function runDoctor(projectName: string | undefined, opts: DoctorDep
|
|
|
1663
1596
|
passFinding("herdr-agent-name", projectProblem === undefined ? "no project resolved — nothing to check" : `agent name uncheckable: ${projectProblem}`),
|
|
1664
1597
|
);
|
|
1665
1598
|
findings.push(
|
|
1666
|
-
passFinding("arm-
|
|
1599
|
+
passFinding("arm-ack", projectProblem === undefined ? "no project resolved — nothing to check" : `arm acknowledgement uncheckable: ${projectProblem}`),
|
|
1667
1600
|
);
|
|
1668
1601
|
findings.push(
|
|
1669
1602
|
passFinding("topic-pin", projectProblem === undefined ? "no project resolved — nothing to check" : `topic pin uncheckable: ${projectProblem}`),
|
|
@@ -1678,7 +1611,7 @@ export async function runDoctor(projectName: string | undefined, opts: DoctorDep
|
|
|
1678
1611
|
for (const p of projects) {
|
|
1679
1612
|
findings.push(await labelProbe(probes, p));
|
|
1680
1613
|
findings.push(herdrAgentNameProbe(probes, p));
|
|
1681
|
-
findings.push(
|
|
1614
|
+
findings.push(armAckProbe(probes, p));
|
|
1682
1615
|
findings.push(topicPinProbe(probes, p));
|
|
1683
1616
|
findings.push(telegramPlumbingProbe(probes, p));
|
|
1684
1617
|
findings.push(ompSettingsProbe(p, sessionRootState));
|
|
@@ -1690,10 +1623,6 @@ export async function runDoctor(projectName: string | undefined, opts: DoctorDep
|
|
|
1690
1623
|
findings.push(unitProbe(probes, projects[0], cfg));
|
|
1691
1624
|
findings.push(recoveryProbe(probes));
|
|
1692
1625
|
findings.push(ownershipProbe(probes));
|
|
1693
|
-
// The worker's path ACLs are a host-wide fact like the unit ownership: one
|
|
1694
|
-
// shared daemon account, one shared worker identity, one shared set of
|
|
1695
|
-
// linked config paths. A finding once, never once per project.
|
|
1696
|
-
findings.push(workerAclProbeFinding(probes));
|
|
1697
1626
|
// #541 seam checks, host-global: the live herdr config and the plugin's
|
|
1698
1627
|
// config.env are single files on the host, not per-project facts.
|
|
1699
1628
|
findings.push(herdrResumeProbe(probes));
|
|
@@ -1737,9 +1666,6 @@ export function defaultProbes(): Probes {
|
|
|
1737
1666
|
readUnit: defaultReadUnit,
|
|
1738
1667
|
stat: defaultStat,
|
|
1739
1668
|
uidOf: defaultUidOf,
|
|
1740
|
-
// The same effective-ACL read the identity plan plans with — doctor and
|
|
1741
|
-
// setup cannot disagree about a worker grant (#835).
|
|
1742
|
-
workerAclHealth: () => workerAclHealth(join(homedir(), ".omp", "agent")),
|
|
1743
1669
|
dbIntegrity: defaultDbIntegrity,
|
|
1744
1670
|
snapshotDirState: defaultSnapshotDirState,
|
|
1745
1671
|
sessionRootState: defaultSessionRootState,
|
package/src/escalate.ts
CHANGED
|
@@ -1040,13 +1040,14 @@ export interface TelegramPlumbingProbe {
|
|
|
1040
1040
|
}
|
|
1041
1041
|
|
|
1042
1042
|
/**
|
|
1043
|
-
* The
|
|
1044
|
-
*
|
|
1045
|
-
*
|
|
1046
|
-
*
|
|
1047
|
-
*
|
|
1048
|
-
*
|
|
1049
|
-
*
|
|
1043
|
+
* The transcript-surface input for {@link telegramPlumbingVerdict}'s session
|
|
1044
|
+
* identity checks: the directories the claim-only proof judges recipients
|
|
1045
|
+
* against (a claim's, the DM owner's, or the local poll owner's session file
|
|
1046
|
+
* must land inside, or a flat reply could land where no challenge would have
|
|
1047
|
+
* looked). Since #614 the *challenge* proof reads conductor acknowledgement
|
|
1048
|
+
* state and never opens a transcript, so callers judging challenge-mode
|
|
1049
|
+
* plumbing omit this — passing it there could report arming impossible while
|
|
1050
|
+
* `arm` succeeds.
|
|
1050
1051
|
*/
|
|
1051
1052
|
export type TelegramPlumbingScan = { dirs: readonly string[] };
|
|
1052
1053
|
|
|
@@ -1074,7 +1075,7 @@ export type TelegramPlumbingScan = { dirs: readonly string[] };
|
|
|
1074
1075
|
*/
|
|
1075
1076
|
export function telegramPlumbingVerdict(
|
|
1076
1077
|
sendTopic: number | undefined,
|
|
1077
|
-
scan: TelegramPlumbingScan,
|
|
1078
|
+
scan: TelegramPlumbingScan | undefined,
|
|
1078
1079
|
probe: TelegramPlumbingProbe,
|
|
1079
1080
|
): TelegramPlumbingVerdict {
|
|
1080
1081
|
const { registry, poll, dmOwner, channel, alive, lockAlive, fresh } = probe;
|
|
@@ -1085,14 +1086,21 @@ export function telegramPlumbingVerdict(
|
|
|
1085
1086
|
const claim = registry.claims.find((c) => c.threadId === sendTopic);
|
|
1086
1087
|
if (claim === undefined) return { ok: false, reason: "no-topic-claim" };
|
|
1087
1088
|
if (claim.pid === undefined || !alive(claim.pid)) return { ok: false, reason: "claim-dead" };
|
|
1088
|
-
if (
|
|
1089
|
+
if (
|
|
1090
|
+
scan !== undefined &&
|
|
1091
|
+
claim.sessionFile !== undefined &&
|
|
1092
|
+
!sessionWithinScan(claim.sessionFile, scan.dirs)
|
|
1093
|
+
) {
|
|
1089
1094
|
return { ok: false, reason: "claim-session-outside" };
|
|
1090
1095
|
}
|
|
1091
1096
|
recipient = "claim";
|
|
1092
1097
|
} else {
|
|
1093
1098
|
if (dmOwner.kind === "ok") {
|
|
1094
1099
|
if (dmOwner.owner.pid === undefined || !alive(dmOwner.owner.pid)) return { ok: false, reason: "dm-owner-dead" };
|
|
1095
|
-
if (
|
|
1100
|
+
if (
|
|
1101
|
+
scan !== undefined &&
|
|
1102
|
+
(dmOwner.owner.sessionFile === undefined || !sessionWithinScan(dmOwner.owner.sessionFile, scan.dirs))
|
|
1103
|
+
) {
|
|
1096
1104
|
return { ok: false, reason: "dm-owner-unrelated" };
|
|
1097
1105
|
}
|
|
1098
1106
|
recipient = "dm-owner";
|
|
@@ -1102,7 +1110,10 @@ export function telegramPlumbingVerdict(
|
|
|
1102
1110
|
// record must name that session. A daemon-only or unrelated owner is no
|
|
1103
1111
|
// recipient: the reply would land in a transcript arm never scans.
|
|
1104
1112
|
if (poll.kind !== "ok") return { ok: false, reason: "no-dm-owner" };
|
|
1105
|
-
if (
|
|
1113
|
+
if (
|
|
1114
|
+
scan !== undefined &&
|
|
1115
|
+
(poll.owner.sessionFile === undefined || !sessionWithinScan(poll.owner.sessionFile, scan.dirs))
|
|
1116
|
+
) {
|
|
1106
1117
|
return { ok: false, reason: "no-dm-owner" };
|
|
1107
1118
|
}
|
|
1108
1119
|
recipient = "poller";
|