@descryy/mcp 0.5.0 → 0.7.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/dist/action-handshake.d.ts +8 -75
- package/dist/action-handshake.d.ts.map +1 -1
- package/dist/action-handshake.js +9 -79
- package/dist/action-handshake.js.map +1 -1
- package/dist/bin/descry-mcp.d.ts +4 -15
- package/dist/bin/descry-mcp.d.ts.map +1 -1
- package/dist/bin/descry-mcp.js +12 -42
- package/dist/bin/descry-mcp.js.map +1 -1
- package/dist/browser/driver.d.ts +62 -178
- package/dist/browser/driver.d.ts.map +1 -1
- package/dist/browser/driver.js +14 -46
- package/dist/browser/driver.js.map +1 -1
- package/dist/browser/evidence.d.ts +11 -35
- package/dist/browser/evidence.d.ts.map +1 -1
- package/dist/browser/evidence.js +25 -51
- package/dist/browser/evidence.js.map +1 -1
- package/dist/browser/fake-driver.d.ts +12 -16
- package/dist/browser/fake-driver.d.ts.map +1 -1
- package/dist/browser/fake-driver.js +24 -17
- package/dist/browser/fake-driver.js.map +1 -1
- package/dist/browser/graph-write.d.ts +4 -45
- package/dist/browser/graph-write.d.ts.map +1 -1
- package/dist/browser/graph-write.js +8 -53
- package/dist/browser/graph-write.js.map +1 -1
- package/dist/browser/playwright-driver.d.ts +17 -67
- package/dist/browser/playwright-driver.d.ts.map +1 -1
- package/dist/browser/playwright-driver.js +169 -166
- package/dist/browser/playwright-driver.js.map +1 -1
- package/dist/browser/provider.d.ts +5 -34
- package/dist/browser/provider.d.ts.map +1 -1
- package/dist/browser/provider.js +4 -24
- package/dist/browser/provider.js.map +1 -1
- package/dist/browser/registry.d.ts +26 -106
- package/dist/browser/registry.d.ts.map +1 -1
- package/dist/browser/registry.js +20 -77
- package/dist/browser/registry.js.map +1 -1
- package/dist/browser/scenario-resolve.d.ts +10 -44
- package/dist/browser/scenario-resolve.d.ts.map +1 -1
- package/dist/browser/scenario-resolve.js +10 -41
- package/dist/browser/scenario-resolve.js.map +1 -1
- package/dist/browser/scenario-runner.d.ts +12 -70
- package/dist/browser/scenario-runner.d.ts.map +1 -1
- package/dist/browser/scenario-runner.js +33 -91
- package/dist/browser/scenario-runner.js.map +1 -1
- package/dist/browser/stack-parser.d.ts +4 -28
- package/dist/browser/stack-parser.d.ts.map +1 -1
- package/dist/browser/stack-parser.js +12 -39
- package/dist/browser/stack-parser.js.map +1 -1
- package/dist/browser/tool-support.d.ts +20 -67
- package/dist/browser/tool-support.d.ts.map +1 -1
- package/dist/browser/tool-support.js +22 -66
- package/dist/browser/tool-support.js.map +1 -1
- package/dist/browser/url-scheme.d.ts +14 -0
- package/dist/browser/url-scheme.d.ts.map +1 -0
- package/dist/browser/url-scheme.js +38 -0
- package/dist/browser/url-scheme.js.map +1 -0
- package/dist/cancellation.d.ts +11 -46
- package/dist/cancellation.d.ts.map +1 -1
- package/dist/cancellation.js +11 -46
- package/dist/cancellation.js.map +1 -1
- package/dist/capped.d.ts +17 -0
- package/dist/capped.d.ts.map +1 -0
- package/dist/capped.js +15 -0
- package/dist/capped.js.map +1 -0
- package/dist/disclosure-ledger.d.ts +6 -30
- package/dist/disclosure-ledger.d.ts.map +1 -1
- package/dist/disclosure-ledger.js +4 -26
- package/dist/disclosure-ledger.js.map +1 -1
- package/dist/index.d.ts +10 -26
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +7 -17
- package/dist/index.js.map +1 -1
- package/dist/module-trust.d.ts +24 -0
- package/dist/module-trust.d.ts.map +1 -0
- package/dist/module-trust.js +60 -0
- package/dist/module-trust.js.map +1 -0
- package/dist/path-confinement.d.ts +31 -0
- package/dist/path-confinement.d.ts.map +1 -0
- package/dist/path-confinement.js +44 -0
- package/dist/path-confinement.js.map +1 -0
- package/dist/protocol.d.ts +10 -53
- package/dist/protocol.d.ts.map +1 -1
- package/dist/protocol.js +14 -60
- package/dist/protocol.js.map +1 -1
- package/dist/registry.d.ts +10 -58
- package/dist/registry.d.ts.map +1 -1
- package/dist/registry.js +38 -90
- package/dist/registry.js.map +1 -1
- package/dist/render.d.ts +79 -11
- package/dist/render.d.ts.map +1 -1
- package/dist/render.js +126 -14
- package/dist/render.js.map +1 -1
- package/dist/runtime-registry.d.ts +8 -49
- package/dist/runtime-registry.d.ts.map +1 -1
- package/dist/runtime-registry.js +72 -63
- package/dist/runtime-registry.js.map +1 -1
- package/dist/scenarios/index.d.ts +1 -1
- package/dist/scenarios/index.d.ts.map +1 -1
- package/dist/scenarios/index.js +1 -1
- package/dist/scenarios/index.js.map +1 -1
- package/dist/scenarios/parse.d.ts +4 -18
- package/dist/scenarios/parse.d.ts.map +1 -1
- package/dist/scenarios/parse.js +14 -34
- package/dist/scenarios/parse.js.map +1 -1
- package/dist/scenarios/scenario.d.ts +18 -74
- package/dist/scenarios/scenario.d.ts.map +1 -1
- package/dist/scenarios/scenario.js +7 -34
- package/dist/scenarios/scenario.js.map +1 -1
- package/dist/scenarios/storage.d.ts +11 -41
- package/dist/scenarios/storage.d.ts.map +1 -1
- package/dist/scenarios/storage.js +57 -47
- package/dist/scenarios/storage.js.map +1 -1
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +55 -13
- package/dist/server.js.map +1 -1
- package/dist/session.d.ts +69 -239
- package/dist/session.d.ts.map +1 -1
- package/dist/session.js +76 -231
- package/dist/session.js.map +1 -1
- package/dist/tools/analyze.d.ts +27 -101
- package/dist/tools/analyze.d.ts.map +1 -1
- package/dist/tools/analyze.js +58 -148
- package/dist/tools/analyze.js.map +1 -1
- package/dist/tools/browser-click.d.ts +3 -21
- package/dist/tools/browser-click.d.ts.map +1 -1
- package/dist/tools/browser-click.js +10 -31
- package/dist/tools/browser-click.js.map +1 -1
- package/dist/tools/browser-close-session.d.ts +4 -13
- package/dist/tools/browser-close-session.d.ts.map +1 -1
- package/dist/tools/browser-close-session.js +4 -13
- package/dist/tools/browser-close-session.js.map +1 -1
- package/dist/tools/browser-fill.d.ts +5 -36
- package/dist/tools/browser-fill.d.ts.map +1 -1
- package/dist/tools/browser-fill.js +9 -44
- package/dist/tools/browser-fill.js.map +1 -1
- package/dist/tools/browser-navigate.d.ts +6 -27
- package/dist/tools/browser-navigate.d.ts.map +1 -1
- package/dist/tools/browser-navigate.js +6 -23
- package/dist/tools/browser-navigate.js.map +1 -1
- package/dist/tools/browser-run-scenario.d.ts +5 -31
- package/dist/tools/browser-run-scenario.d.ts.map +1 -1
- package/dist/tools/browser-run-scenario.js +10 -46
- package/dist/tools/browser-run-scenario.js.map +1 -1
- package/dist/tools/browser-save-scenario.d.ts +3 -28
- package/dist/tools/browser-save-scenario.d.ts.map +1 -1
- package/dist/tools/browser-save-scenario.js +102 -55
- package/dist/tools/browser-save-scenario.js.map +1 -1
- package/dist/tools/browser-snapshot.d.ts +6 -39
- package/dist/tools/browser-snapshot.d.ts.map +1 -1
- package/dist/tools/browser-snapshot.js +4 -31
- package/dist/tools/browser-snapshot.js.map +1 -1
- package/dist/tools/browser-start-session.d.ts +4 -23
- package/dist/tools/browser-start-session.d.ts.map +1 -1
- package/dist/tools/browser-start-session.js +38 -51
- package/dist/tools/browser-start-session.js.map +1 -1
- package/dist/tools/browser-type.d.ts +5 -35
- package/dist/tools/browser-type.d.ts.map +1 -1
- package/dist/tools/browser-type.js +10 -45
- package/dist/tools/browser-type.js.map +1 -1
- package/dist/tools/contracts.d.ts +17 -63
- package/dist/tools/contracts.d.ts.map +1 -1
- package/dist/tools/contracts.js +91 -47
- package/dist/tools/contracts.js.map +1 -1
- package/dist/tools/cross-pr.d.ts +56 -15
- package/dist/tools/cross-pr.d.ts.map +1 -1
- package/dist/tools/cross-pr.js +141 -38
- package/dist/tools/cross-pr.js.map +1 -1
- package/dist/tools/git-diff.d.ts +18 -2
- package/dist/tools/git-diff.d.ts.map +1 -1
- package/dist/tools/git-diff.js +143 -23
- package/dist/tools/git-diff.js.map +1 -1
- package/dist/tools/git-history.d.ts +5 -16
- package/dist/tools/git-history.d.ts.map +1 -1
- package/dist/tools/git-history.js +3 -10
- package/dist/tools/git-history.js.map +1 -1
- package/dist/tools/history.d.ts +4 -33
- package/dist/tools/history.d.ts.map +1 -1
- package/dist/tools/history.js +6 -31
- package/dist/tools/history.js.map +1 -1
- package/dist/tools/impact.d.ts +7 -51
- package/dist/tools/impact.d.ts.map +1 -1
- package/dist/tools/impact.js +15 -67
- package/dist/tools/impact.js.map +1 -1
- package/dist/tools/index.d.ts +3 -8
- package/dist/tools/index.d.ts.map +1 -1
- package/dist/tools/index.js +3 -8
- package/dist/tools/index.js.map +1 -1
- package/dist/tools/kit.d.ts +66 -113
- package/dist/tools/kit.d.ts.map +1 -1
- package/dist/tools/kit.js +60 -28
- package/dist/tools/kit.js.map +1 -1
- package/dist/tools/link-workspace.d.ts +7 -45
- package/dist/tools/link-workspace.d.ts.map +1 -1
- package/dist/tools/link-workspace.js +10 -48
- package/dist/tools/link-workspace.js.map +1 -1
- package/dist/tools/lookup.d.ts +4 -14
- package/dist/tools/lookup.d.ts.map +1 -1
- package/dist/tools/lookup.js +4 -14
- package/dist/tools/lookup.js.map +1 -1
- package/dist/tools/mark-incident.d.ts +7 -54
- package/dist/tools/mark-incident.d.ts.map +1 -1
- package/dist/tools/mark-incident.js +15 -68
- package/dist/tools/mark-incident.js.map +1 -1
- package/dist/tools/observe-runtime.d.ts +27 -210
- package/dist/tools/observe-runtime.d.ts.map +1 -1
- package/dist/tools/observe-runtime.js +258 -436
- package/dist/tools/observe-runtime.js.map +1 -1
- package/dist/tools/observe-tests.d.ts +9 -88
- package/dist/tools/observe-tests.d.ts.map +1 -1
- package/dist/tools/observe-tests.js +34 -108
- package/dist/tools/observe-tests.js.map +1 -1
- package/dist/tools/pr-analysis.d.ts +18 -2
- package/dist/tools/pr-analysis.d.ts.map +1 -1
- package/dist/tools/pr-analysis.js +73 -23
- package/dist/tools/pr-analysis.js.map +1 -1
- package/dist/tools/pre-push.d.ts +94 -9
- package/dist/tools/pre-push.d.ts.map +1 -1
- package/dist/tools/pre-push.js +139 -34
- package/dist/tools/pre-push.js.map +1 -1
- package/dist/tools/propagation.d.ts +11 -53
- package/dist/tools/propagation.d.ts.map +1 -1
- package/dist/tools/propagation.js +14 -57
- package/dist/tools/propagation.js.map +1 -1
- package/dist/tools/questions.d.ts +13 -63
- package/dist/tools/questions.d.ts.map +1 -1
- package/dist/tools/questions.js +33 -105
- package/dist/tools/questions.js.map +1 -1
- package/dist/tools/refusal-fetch.d.ts +4 -40
- package/dist/tools/refusal-fetch.d.ts.map +1 -1
- package/dist/tools/refusal-fetch.js +4 -40
- package/dist/tools/refusal-fetch.js.map +1 -1
- package/dist/tools/runtime-incident.d.ts +4 -63
- package/dist/tools/runtime-incident.d.ts.map +1 -1
- package/dist/tools/runtime-incident.js +10 -87
- package/dist/tools/runtime-incident.js.map +1 -1
- package/dist/tools/scope.d.ts +7 -25
- package/dist/tools/scope.d.ts.map +1 -1
- package/dist/tools/scope.js +17 -20
- package/dist/tools/scope.js.map +1 -1
- package/dist/tools/similar-incidents.d.ts +11 -86
- package/dist/tools/similar-incidents.d.ts.map +1 -1
- package/dist/tools/similar-incidents.js +7 -71
- package/dist/tools/similar-incidents.js.map +1 -1
- package/dist/tools/validate.d.ts +31 -47
- package/dist/tools/validate.d.ts.map +1 -1
- package/dist/tools/validate.js +133 -61
- package/dist/tools/validate.js.map +1 -1
- package/dist/tools/verification-status.d.ts +9 -64
- package/dist/tools/verification-status.d.ts.map +1 -1
- package/dist/tools/verification-status.js +9 -62
- package/dist/tools/verification-status.js.map +1 -1
- package/dist/tools/verify-claim.d.ts +5 -52
- package/dist/tools/verify-claim.d.ts.map +1 -1
- package/dist/tools/verify-claim.js +6 -56
- package/dist/tools/verify-claim.js.map +1 -1
- package/dist/transport.d.ts +15 -52
- package/dist/transport.d.ts.map +1 -1
- package/dist/transport.js +16 -60
- package/dist/transport.js.map +1 -1
- package/package.json +33 -16
|
@@ -1,137 +1,22 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* `observe_runtime` — boot or attach to a real application,
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
* re-index can reproduce.** `analyze` re-derives the graph from source already
|
|
7
|
-
* on disk, so nothing it writes is a new claim about the world. This one runs
|
|
8
|
-
* a real process and records what it saw happen, which is the one accuracy
|
|
9
|
-
* source architecture §11B.3 calls *"the core technical moat"* and the one a
|
|
10
|
-
* purely static tool is structurally unable to reach: *"a purely static
|
|
11
|
-
* code-graph tool is capped at R3 permanently. It has no runtime."*
|
|
12
|
-
*
|
|
13
|
-
* ## What this composes, and what it invents
|
|
14
|
-
*
|
|
15
|
-
* It invents no mechanism. Every stage already existed, gate-verified, in
|
|
16
|
-
* `descry-runtime`, and every one of them was dormant — the whole point of
|
|
17
|
-
* `DEC-NEXT-mcp-runtime-dependency-boundary-for-r4-evidence`, which measured
|
|
18
|
-
* that `applyRuntimeObservations` had **zero production callers anywhere**,
|
|
19
|
-
* not in `descry-desktop` and not in `descry-runtime`'s own pipeline. Four
|
|
20
|
-
* shipped components in a row, and this tool is the wire between them:
|
|
21
|
-
*
|
|
22
|
-
* 1. `runInstrumentedExecution` (`@descryy/runtime-orchestrator`) spawns or
|
|
23
|
-
* attaches the declared services, starts the adapter's collectors, drains
|
|
24
|
-
* them for a stated window, and writes every item through `EvidenceStore`.
|
|
25
|
-
* 2. `correlateExecution` (`@descryy/runtime-evidence-correlation`) resolves
|
|
26
|
-
* each evidence item to the graph node it is *about* — the resolve-then-
|
|
27
|
-
* attribute pass. This answers identity, not edges.
|
|
28
|
-
* 3. `confirmObservedFrontendCaller` (`@descryy/runtime-graph-correlator`)
|
|
29
|
-
* turns a captured call-site stack plus a resolved endpoint into a
|
|
30
|
-
* `RuntimeEdgeObservation`, and
|
|
31
|
-
* 4. calls `applyRuntimeObservations` (`@descryy/core`) with it, which
|
|
32
|
-
* promotes, mints or contradicts the edge and writes the EARNED ledger.
|
|
33
|
-
*
|
|
34
|
-
* ## Why stage 3 exists rather than deriving edges from stage 2 directly
|
|
35
|
-
*
|
|
36
|
-
* `uat-phase-1-bug-fixes.md` Phase 2 describes step 2 as producing
|
|
37
|
-
* `RuntimeEdgeObservation[]`. It does not, and the difference is load-bearing
|
|
38
|
-
* rather than pedantic: `correlateExecution` returns
|
|
39
|
-
* `CorrelationAttribution`s — *(evidenceId, graphNodeId)* pairs saying which
|
|
40
|
-
* single node an observation is about. An edge needs **two** endpoints and a
|
|
41
|
-
* witnessed relationship between them, and manufacturing one by pairing up
|
|
42
|
-
* attributions that happen to co-occur in the same run would mint edges from
|
|
43
|
-
* temporal coincidence. That is precisely the wrong-direction failure rule 2
|
|
44
|
-
* exists to prevent, arriving through the one mechanism built to make the
|
|
45
|
-
* graph *more* trustworthy.
|
|
46
|
-
*
|
|
47
|
-
* So the observation comes from the one shipped producer that can honestly
|
|
48
|
-
* make one: a captured stack naming the caller, against an endpoint the same
|
|
49
|
-
* observation named. Both endpoints come from the observation itself. Every
|
|
50
|
-
* other correlated item is reported in the counts and produces no edge, which
|
|
51
|
-
* is a disclosed gap rather than a silent one.
|
|
52
|
-
*
|
|
53
|
-
* ## What this closure can actually witness today — measured, not assumed
|
|
54
|
-
*
|
|
55
|
-
* The wire is complete, and what it can carry changed when the runtime packages
|
|
56
|
-
* were published at 0.1.0 and pinned here — so this paragraph is the record of
|
|
57
|
-
* a limit that was real and is now lifted, kept rather than deleted because the
|
|
58
|
-
* shape of it recurs.
|
|
59
|
-
*
|
|
60
|
-
* **It used to be that no producer of the required pair — HTTP evidence
|
|
61
|
-
* carrying a call-site stack — was in this server's closure.** The browser
|
|
62
|
-
* network collector never has been. The outbound-fetch instrumentation in
|
|
63
|
-
* `@descryy/runtime-external-service-observation` existed, was proven in
|
|
64
|
-
* `descry-runtime`, and was not installed here at all. So a run resolved both
|
|
65
|
-
* kinds of node and wrote no edge, and `STANDING_NOTES` said so on every call
|
|
66
|
-
* because "no edge was written" and "nothing here could have written one" are
|
|
67
|
-
* different statements.
|
|
68
|
-
*
|
|
69
|
-
* **That second producer is now in the closure**, transitively through
|
|
70
|
-
* `@descryy/runtime-orchestrator`, which applies the adapter's
|
|
71
|
-
* `outboundHttpLaunch()` between the interpreter and the script so the client
|
|
72
|
-
* is patched before any application code can capture an unpatched one. Measured
|
|
73
|
-
* end to end in `observe-runtime-outbound.conformance.test.ts`: two real
|
|
74
|
-
* processes, 17 `EXTERNAL_REQUEST` items alongside 18 `BACKEND_LOG`, one
|
|
75
|
-
* `USES_API` edge minted at R4, and the first `strongly supported` reply this
|
|
76
|
-
* server has produced.
|
|
77
|
-
*
|
|
78
|
-
* **Two limits remain, and they are stated rather than inferred from a zero.**
|
|
79
|
-
* An *attached* service is not launched by Descry, so the instrumentation
|
|
80
|
-
* cannot be installed into it and its outbound calls carry no stack. And
|
|
81
|
-
* browser-side traffic still needs `@descryy/runtime-browser`, which is not
|
|
82
|
-
* here. Both are in `STANDING_NOTES`.
|
|
83
|
-
*
|
|
84
|
-
* The rule that outlives all of it: a disclosure about the closure is a fact
|
|
85
|
-
* about *this build*, not about what `descry-runtime` can do. The two came
|
|
86
|
-
* apart once already, when the cross-boundary lane landed and this file still
|
|
87
|
-
* claimed the capability was absent. Re-check it against the installed tree
|
|
88
|
-
* when the pins move, not against the source repository.
|
|
89
|
-
* Descry.
|
|
90
|
-
*
|
|
91
|
-
* ## Why no denial is ever emitted
|
|
92
|
-
*
|
|
93
|
-
* `applyRuntimeObservations` accepts `held: false`. Nothing here ever sends
|
|
94
|
-
* one, and `confirmObservedFrontendCaller`'s own header explains why: a run
|
|
95
|
-
* establishes that a call *happened*; it cannot establish that one did not,
|
|
96
|
-
* because a run exercises the paths it happens to take. Demoting a correct
|
|
97
|
-
* edge on the strength of a route this run did not visit would be worse than
|
|
98
|
-
* never running.
|
|
99
|
-
*
|
|
100
|
-
* ## Class and tier
|
|
101
|
-
*
|
|
102
|
-
* `action` — DEC-278's own test is *"can this call's effect change a later
|
|
103
|
-
* finding without the developer having said so"*, and this one spawns
|
|
104
|
-
* processes and writes R4 edges that cap every later reliability class
|
|
105
|
-
* differently. It is gated by the same two-call `confirmToken` handshake
|
|
106
|
-
* `questions` uses, and additionally by the environment profile's declared
|
|
107
|
-
* `safetyLevel` (DEC-270): booting a service is a **write** against the
|
|
108
|
-
* target, so a `readOnly` profile refuses. An all-attach run is genuinely
|
|
109
|
-
* read-only — `ServiceConfiguration.attach`'s own contract is that Descry
|
|
110
|
-
* never executes code in, or applies limits to, a process it did not spawn —
|
|
111
|
-
* so it is allowed under `readOnly`, and that distinction is stated in the
|
|
112
|
-
* disclosures rather than inferred silently.
|
|
113
|
-
*
|
|
114
|
-
* `evidence` — it reports what was witnessed and what was written. It draws no
|
|
115
|
-
* conclusion about the user's code; nothing here reads or writes a finding, a
|
|
116
|
-
* hypothesis or a category (RT-027).
|
|
117
|
-
*
|
|
118
|
-
* ## No new query tools
|
|
119
|
-
*
|
|
120
|
-
* None are needed and none are added. `impact`, `propagation` and the rest
|
|
121
|
-
* already read the `resolution` field, so an edge this tool promotes to R4
|
|
122
|
-
* becomes visible through every one of them on the next call, with no change
|
|
123
|
-
* to any of them.
|
|
124
|
-
*/
|
|
2
|
+
* `observe_runtime` — boot or attach to a real application, write what was witnessed into the
|
|
3
|
+
* graph as R4 facts. Composes existing descry-runtime stages, no new mechanism (DEC-NEXT-mcp-
|
|
4
|
+
* runtime-dependency-boundary-for-r4-evidence). `action`/`evidence` (DEC-278), gated by
|
|
5
|
+
* confirmToken + profile safetyLevel (DEC-270). Never emits a denial. */
|
|
125
6
|
import { mkdir } from "node:fs/promises";
|
|
126
7
|
import { dirname, isAbsolute, join } from "node:path";
|
|
127
8
|
import { independentSignalTypes } from "@descryy/ir";
|
|
9
|
+
import { checkNotSensitivePath, checkPathConfinement } from "../path-confinement.js";
|
|
10
|
+
/** A-F6 escape hatches — see path-confinement.ts. */
|
|
11
|
+
const ALLOW_EVIDENCE_PATH_OUTSIDE_REPO_ENV_VAR = "DESCRY_ALLOW_EVIDENCE_PATH_OUTSIDE_REPO";
|
|
12
|
+
const ALLOW_SENSITIVE_LOG_PATH_ENV_VAR = "DESCRY_ALLOW_SENSITIVE_LOG_PATH";
|
|
128
13
|
import { buildGraph, counts, createConfirmedIncidentSource, persistGraph, } from "@descryy/core";
|
|
129
14
|
import { evaluateAction, validateProfile } from "@descryy/runtime-environment-profile";
|
|
130
15
|
import { correlateExecution } from "@descryy/runtime-evidence-correlation";
|
|
131
16
|
import { EvidenceStore } from "@descryy/runtime-evidence-store";
|
|
132
17
|
import { confirmObservedFrontendCaller } from "@descryy/runtime-graph-correlator";
|
|
133
18
|
import { runInstrumentedExecution } from "@descryy/runtime-orchestrator";
|
|
134
|
-
import { answer, optionalInteger, optionalString, ToolInputError, } from "./kit.js";
|
|
19
|
+
import { answer, optionalEnum, optionalInteger, optionalString, ToolInputError, } from "./kit.js";
|
|
135
20
|
import { cancellationHeadline, cancellationNotes, whenAborted } from "../cancellation.js";
|
|
136
21
|
import { loadRuntimeAdapter, RuntimeAdapterLoadError } from "../runtime-registry.js";
|
|
137
22
|
import { writeConfirmedIncident } from "../session.js";
|
|
@@ -186,10 +71,8 @@ const SCHEMA = {
|
|
|
186
71
|
"an \"http\" or \"tcp-port\" check it is not needed at all, because nothing is " +
|
|
187
72
|
"started and Descry never executes code in a process it did not spawn.",
|
|
188
73
|
},
|
|
189
|
-
//
|
|
190
|
-
//
|
|
191
|
-
// investigation, all the same root cause wearing different clothes,
|
|
192
|
-
// and the explanation both times living in a source comment.
|
|
74
|
+
// Fixes the tool's costliest ergonomic gap: 3 failed runs in one real investigation,
|
|
75
|
+
// same root cause, explanation previously living only in a source comment.
|
|
193
76
|
port: {
|
|
194
77
|
type: "integer",
|
|
195
78
|
description: "The port readiness checks against, and the two modes need opposite things from you. " +
|
|
@@ -306,6 +189,51 @@ const SCHEMA = {
|
|
|
306
189
|
type: "string",
|
|
307
190
|
description: `Where the evidence database lives. Defaults to ${DEFAULT_EVIDENCE_RELATIVE_PATH} under the repository root.`,
|
|
308
191
|
},
|
|
192
|
+
resourceLimits: {
|
|
193
|
+
type: "object",
|
|
194
|
+
description: "A-F5: caps on a spawned service, enforced by the OS (prlimit) — never applied to an attached " +
|
|
195
|
+
"service, since Descry did not start it. Absent means unconstrained, which every reply discloses.",
|
|
196
|
+
properties: {
|
|
197
|
+
maxMemoryBytes: { type: "integer", description: "Virtual address space cap (prlimit --as)." },
|
|
198
|
+
maxCpuSeconds: { type: "integer", description: "CPU time cap, in seconds (prlimit --cpu)." },
|
|
199
|
+
maxProcesses: { type: "integer", description: "Process count cap, per real uid (prlimit --nproc)." },
|
|
200
|
+
},
|
|
201
|
+
additionalProperties: false,
|
|
202
|
+
},
|
|
203
|
+
filesystemPolicy: {
|
|
204
|
+
type: "object",
|
|
205
|
+
description: "A-F5: confines a spawned service's filesystem view to its own cwd plus these roots — real on " +
|
|
206
|
+
"Linux (a bwrap mount namespace; everything else is not merely unreadable, it is not mounted at " +
|
|
207
|
+
"all), refused rather than silently unenforced elsewhere. Absent means unconstrained.",
|
|
208
|
+
properties: {
|
|
209
|
+
allowedRoots: {
|
|
210
|
+
type: "array",
|
|
211
|
+
items: { type: "string" },
|
|
212
|
+
description: "Absolute paths visible read-write in addition to the service's own cwd.",
|
|
213
|
+
},
|
|
214
|
+
},
|
|
215
|
+
required: ["allowedRoots"],
|
|
216
|
+
additionalProperties: false,
|
|
217
|
+
},
|
|
218
|
+
networkPolicy: {
|
|
219
|
+
type: "object",
|
|
220
|
+
description: 'A-F5: only { mode: "allow", hosts: [] } (full denial) is actually enforced today — a network ' +
|
|
221
|
+
"namespace holding nothing but an unreachable loopback. Any other shape refuses the run rather " +
|
|
222
|
+
"than starting unconstrained under a policy nobody enforced. Absent means unconstrained.",
|
|
223
|
+
properties: {
|
|
224
|
+
mode: { type: "string", enum: ["allow", "deny"] },
|
|
225
|
+
hosts: { type: "array", items: { type: "string" } },
|
|
226
|
+
},
|
|
227
|
+
required: ["mode", "hosts"],
|
|
228
|
+
additionalProperties: false,
|
|
229
|
+
},
|
|
230
|
+
sandboxBackend: {
|
|
231
|
+
type: "string",
|
|
232
|
+
enum: ["bwrap", "container"],
|
|
233
|
+
description: 'Which mechanism enforces filesystemPolicy/networkPolicy. Defaults to "bwrap" (Linux-native). ' +
|
|
234
|
+
'"container" routes through a real Docker container instead — the only option on macOS/Windows, ' +
|
|
235
|
+
"and it does not compose with resourceLimits (disclosed on the reply when both are declared).",
|
|
236
|
+
},
|
|
309
237
|
confirmToken: {
|
|
310
238
|
type: "string",
|
|
311
239
|
description: "The token returned by an unconfirmed call. This tool performs nothing without it: the first " +
|
|
@@ -317,9 +245,6 @@ const SCHEMA = {
|
|
|
317
245
|
required: ["profile", "services", "adapter"],
|
|
318
246
|
additionalProperties: false,
|
|
319
247
|
};
|
|
320
|
-
// ---------------------------------------------------------------------------
|
|
321
|
-
// Argument reading. Hand-written, same reasoning as `kit.ts`'s own readers.
|
|
322
|
-
// ---------------------------------------------------------------------------
|
|
323
248
|
function asRecord(value, what) {
|
|
324
249
|
if (typeof value !== "object" || value === null || Array.isArray(value)) {
|
|
325
250
|
throw new ToolInputError(`"${what}" must be an object`);
|
|
@@ -343,8 +268,7 @@ function readProfile(args) {
|
|
|
343
268
|
};
|
|
344
269
|
const errors = validateProfile(candidate);
|
|
345
270
|
if (errors.length > 0) {
|
|
346
|
-
// The profile package's own error codes, verbatim —
|
|
347
|
-
// interpretation to a validation it did not perform.
|
|
271
|
+
// The profile package's own error codes, verbatim — no interpretation added here.
|
|
348
272
|
throw new ToolInputError(`"profile" is not valid: ${errors.join(", ")}`);
|
|
349
273
|
}
|
|
350
274
|
return candidate;
|
|
@@ -385,25 +309,13 @@ function readServices(args, repoPath) {
|
|
|
385
309
|
if (command !== undefined && typeof command !== "string") {
|
|
386
310
|
throw new ToolInputError(`"services.${name}.command" must be a string`);
|
|
387
311
|
}
|
|
388
|
-
// Read once
|
|
389
|
-
// `cwd` is required at all, and whether an attached service must state its
|
|
390
|
-
// port.
|
|
312
|
+
// Read once: two rules below need it (whether cwd is required, whether attach needs a port).
|
|
391
313
|
const readinessRaw = entry["readiness"];
|
|
392
314
|
const readinessKind = typeof readinessRaw === "object" && readinessRaw !== null && !Array.isArray(readinessRaw)
|
|
393
315
|
? readinessRaw["kind"]
|
|
394
316
|
: undefined;
|
|
395
|
-
//
|
|
396
|
-
//
|
|
397
|
-
// executes code in a process it did not spawn, so for an attached service
|
|
398
|
-
// with an `http` or `tcp-port` check the value is inert — demanding it
|
|
399
|
-
// makes the caller invent a path that changes nothing. A `command` check
|
|
400
|
-
// *is* executed, in this directory, so it still needs one.
|
|
401
|
-
//
|
|
402
|
-
// The runtime contract's `ServiceConfiguration.cwd` is non-optional, so
|
|
403
|
-
// something must be supplied downstream either way; the repository root is
|
|
404
|
-
// the inert choice, and it is inert precisely because nothing runs there
|
|
405
|
-
// on this path.
|
|
406
|
-
// (`DEC-388`.)
|
|
317
|
+
// cwd required only where something executes in it: attach never runs code Descry didn't
|
|
318
|
+
// spawn, so http/tcp-port checks leave it inert; a "command" check does execute (DEC-388).
|
|
407
319
|
const cwdRaw = entry["cwd"];
|
|
408
320
|
if (cwdRaw !== undefined && (typeof cwdRaw !== "string" || cwdRaw === "")) {
|
|
409
321
|
throw new ToolInputError(`"services.${name}.cwd" must be a non-empty string`);
|
|
@@ -447,20 +359,17 @@ function readServices(args, repoPath) {
|
|
|
447
359
|
if (typeof logFilePath !== "string" || logFilePath === "") {
|
|
448
360
|
throw new ToolInputError(`"services.${name}.attach.logFilePath" is required`);
|
|
449
361
|
}
|
|
362
|
+
// A-F6: logFilePath is read in full on every poll and can't be root-confined (a real
|
|
363
|
+
// log lives anywhere) — narrower, disclosed name-based check instead (path-confinement.ts).
|
|
364
|
+
const sensitivity = checkNotSensitivePath(logFilePath, ALLOW_SENSITIVE_LOG_PATH_ENV_VAR);
|
|
365
|
+
if (sensitivity.sensitive) {
|
|
366
|
+
throw new ToolInputError(`"services.${name}.attach.logFilePath": ${sensitivity.reason}`);
|
|
367
|
+
}
|
|
450
368
|
attach = { pid, logFilePath };
|
|
451
|
-
//
|
|
452
|
-
//
|
|
453
|
-
//
|
|
454
|
-
//
|
|
455
|
-
// as `port ?? 0`, it surfaces to a caller as a readiness check timing
|
|
456
|
-
// out against port 0 some seconds later, with the actual explanation
|
|
457
|
-
// living in a source comment they cannot see. Twice in one real
|
|
458
|
-
// investigation that cost a full failed run to rediscover. So it is
|
|
459
|
-
// refused here, by name, before anything starts.
|
|
460
|
-
//
|
|
461
|
-
// Only for the two checks that resolve a port. A "command" check runs an
|
|
462
|
-
// executable and never asks where the service listens, so demanding a
|
|
463
|
-
// port for it would be a second wrong answer in the other direction.
|
|
369
|
+
// An attached service's port can't be allocated or inferred — nothing in a pid or log
|
|
370
|
+
// path reveals it. Left as `port ?? 0` it silently times out against port 0; refused
|
|
371
|
+
// here by name instead (cost a full failed run to rediscover, twice). Not for "command"
|
|
372
|
+
// checks, which never ask where the service listens.
|
|
464
373
|
if ((readinessKind === "http" || readinessKind === "tcp-port") && port === undefined) {
|
|
465
374
|
throw new ToolInputError(`"services.${name}.port" is required when "${name}" uses "attach" with a ` +
|
|
466
375
|
`"${readinessKind}" readiness check: the check needs a port and an attached target's ` +
|
|
@@ -484,16 +393,9 @@ function readServices(args, repoPath) {
|
|
|
484
393
|
};
|
|
485
394
|
});
|
|
486
395
|
}
|
|
487
|
-
/**
|
|
488
|
-
*
|
|
489
|
-
*
|
|
490
|
-
* `log-pattern` needs a `read()` closing over the `ManagedProcess` the
|
|
491
|
-
* controller owns, and `custom-hook` is a function outright. Neither survives a
|
|
492
|
-
* JSON boundary, and inventing a string-shaped stand-in for either would offer
|
|
493
|
-
* a mechanism that silently is not the one named. They are absent from the
|
|
494
|
-
* schema's enum and stated in the disclosures instead — rule 7, honest
|
|
495
|
-
* degradation, applied to a capability rather than to a result.
|
|
496
|
-
*/
|
|
396
|
+
/** JSON→ReadinessCheck mapping. `log-pattern`/`custom-hook` can't survive a JSON boundary
|
|
397
|
+
* (one closes over a live process, the other is a function) — absent from the enum and
|
|
398
|
+
* stated in disclosures instead (rule 7, applied to a capability, not just a result). */
|
|
497
399
|
function readReadiness(raw, service, cwd) {
|
|
498
400
|
const entry = asRecord(raw, `services.${service}.readiness`);
|
|
499
401
|
const kind = entry["kind"];
|
|
@@ -505,12 +407,8 @@ function readReadiness(raw, service, cwd) {
|
|
|
505
407
|
(typeof timeoutMs !== "number" || !Number.isInteger(timeoutMs) || timeoutMs < 1)) {
|
|
506
408
|
throw new ToolInputError(`"services.${service}.readiness.timeoutMs" must be a positive integer`);
|
|
507
409
|
}
|
|
508
|
-
//
|
|
509
|
-
//
|
|
510
|
-
// argument validated lazily would surface as a failed run with a live process
|
|
511
|
-
// to clean up rather than as a rejected call that started nothing — and
|
|
512
|
-
// `ToolInputError`'s whole contract is that it is something the caller can
|
|
513
|
-
// fix before anything happens.
|
|
410
|
+
// Validated here, not lazily inside checks() — the controller only calls checks() after
|
|
411
|
+
// spawning, so a bad argument caught late means a live process to clean up.
|
|
514
412
|
const path = typeof entry["path"] === "string" ? entry["path"] : "/";
|
|
515
413
|
const expectedStatus = entry["expectedStatus"];
|
|
516
414
|
if (expectedStatus !== undefined && typeof expectedStatus !== "number") {
|
|
@@ -584,9 +482,6 @@ function readScopes(args, declared, fallback) {
|
|
|
584
482
|
}
|
|
585
483
|
return scopes;
|
|
586
484
|
}
|
|
587
|
-
// ---------------------------------------------------------------------------
|
|
588
|
-
// The run
|
|
589
|
-
// ---------------------------------------------------------------------------
|
|
590
485
|
const EMPTY_WRITE = {
|
|
591
486
|
promoted: [],
|
|
592
487
|
created: [],
|
|
@@ -595,14 +490,9 @@ const EMPTY_WRITE = {
|
|
|
595
490
|
contradictions: [],
|
|
596
491
|
staleR4: [],
|
|
597
492
|
};
|
|
598
|
-
/**
|
|
599
|
-
*
|
|
600
|
-
*
|
|
601
|
-
* Stated unconditionally rather than only when it bites: a caller who does not
|
|
602
|
-
* know that `log-pattern` readiness is unreachable here will write a
|
|
603
|
-
* `tcp-port` check that passes the instant the socket binds and read the
|
|
604
|
-
* resulting empty evidence as "the service produced nothing".
|
|
605
|
-
*/
|
|
493
|
+
/** Stated unconditionally on every call, not only when it bites: a caller unaware
|
|
494
|
+
* log-pattern readiness is unreachable will misread a tcp-port check's empty evidence
|
|
495
|
+
* as "the service produced nothing". */
|
|
606
496
|
const STANDING_NOTES = [
|
|
607
497
|
"Readiness here offers only the three mechanisms JSON can state — http, tcp-port and command. " +
|
|
608
498
|
"log-pattern and custom-hook need a function and are unreachable through this tool; a run that " +
|
|
@@ -625,30 +515,40 @@ const STANDING_NOTES = [
|
|
|
625
515
|
"No denial is ever recorded. A run establishes that a call happened; it cannot establish that one " +
|
|
626
516
|
"did not, because it exercises only the paths it took. Nothing here demotes an edge.",
|
|
627
517
|
];
|
|
628
|
-
/**
|
|
629
|
-
*
|
|
630
|
-
*
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
518
|
+
/** A-F5: honest degradation about real isolation, said on every reply rather than left for
|
|
519
|
+
* the caller to discover. applySandbox enforces resourceLimits/filesystemPolicy/networkPolicy
|
|
520
|
+
* for real on Linux when declared; never applied to an attached service (Descry didn't spawn it). */
|
|
521
|
+
function sandboxDisclosure(options) {
|
|
522
|
+
const { resourceLimits, filesystemPolicy, networkPolicy, sandboxBackend } = options;
|
|
523
|
+
// True regardless of policy: spawnProcess merges the full ambient env into every spawned
|
|
524
|
+
// process and bwrap doesn't clear it — out of this repo's reach (DEC-NEXT-runtime-sandbox-residual-gaps).
|
|
525
|
+
const envCaveat = "This is unaffected by any policy above: the spawned process still receives this operator's full " +
|
|
526
|
+
"environment, secrets included — the merge happens in @descryy/runtime-controller's spawnProcess and " +
|
|
527
|
+
"cannot be narrowed from this server.";
|
|
528
|
+
if (resourceLimits === undefined && filesystemPolicy === undefined && networkPolicy === undefined) {
|
|
529
|
+
return ("No resourceLimits, filesystemPolicy or networkPolicy was declared for this run. Every spawned " +
|
|
530
|
+
"service therefore ran with no OS-enforced isolation: it can read and write anything this operator's " +
|
|
531
|
+
"account can, use as much memory/CPU/process count as the host allows, and reach any network this " +
|
|
532
|
+
"operator's account can reach, and it received this operator's full environment. Attached services " +
|
|
533
|
+
"are never sandboxed regardless — Descry did not spawn them. Declare resourceLimits/filesystemPolicy/" +
|
|
534
|
+
"networkPolicy to change the filesystem/network/resource part of this for a spawned service.");
|
|
535
|
+
}
|
|
536
|
+
const applied = [];
|
|
537
|
+
if (resourceLimits !== undefined)
|
|
538
|
+
applied.push("resourceLimits");
|
|
539
|
+
if (filesystemPolicy !== undefined)
|
|
540
|
+
applied.push(`filesystemPolicy (allowedRoots: ${filesystemPolicy.allowedRoots.join(", ") || "none beyond cwd"})`);
|
|
541
|
+
if (networkPolicy !== undefined)
|
|
542
|
+
applied.push(`networkPolicy (${networkPolicy.mode}: ${networkPolicy.hosts.join(", ") || "none"})`);
|
|
543
|
+
const backend = sandboxBackend ?? "bwrap";
|
|
544
|
+
return (`Declared for this run, applied to every spawned (never attached) service via the "${backend}" ` +
|
|
545
|
+
`backend: ${applied.join(", ")}. If any of these could not actually be enforced (wrong platform, an ` +
|
|
546
|
+
"unsupported networkPolicy shape, bwrap/Docker unavailable), the run refused to start rather than " +
|
|
547
|
+
`running unconstrained under a policy nobody enforced — see the headline if this reply is a refusal. ${envCaveat}`);
|
|
548
|
+
}
|
|
549
|
+
/** Said on every attaching run. Measured: a redirected process's stdout is block-buffered,
|
|
550
|
+
* not line-buffered — 3 lines written over 0.6s were still absent from the file 3s later.
|
|
551
|
+
* Nothing in Descry can see those bytes; a boundary to state, not a gap to close. */
|
|
652
552
|
const ATTACH_BUFFERING_NOTE = "Attaching reads a file the target writes; it can only see what the target has already flushed " +
|
|
653
553
|
"there. A process whose output is redirected to a file is usually block-buffered rather than " +
|
|
654
554
|
"line-buffered — its own runtime holds whole lines in a userspace buffer, invisible from outside, " +
|
|
@@ -656,26 +556,58 @@ const ATTACH_BUFFERING_NOTE = "Attaching reads a file the target writes; it can
|
|
|
656
556
|
"arrive in the file after the window closed and be absent here, which is a fact about the " +
|
|
657
557
|
"target's buffering and not evidence that it did nothing. Run the target with its output " +
|
|
658
558
|
"unbuffered or line-buffered if the timing matters.";
|
|
659
|
-
/**
|
|
660
|
-
*
|
|
661
|
-
*
|
|
662
|
-
|
|
663
|
-
*
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
559
|
+
/** Every argument this tool takes (readArguments, below), extracted so describeAction can
|
|
560
|
+
* call it too — catches a bad argument before minting a token for a run that could never
|
|
561
|
+
* happen (DEC-387). Touches nothing; exported for its own test. */
|
|
562
|
+
/** A-F5: ResourceLimits, straight through to ExecutionConfiguration. applyResourceLimits
|
|
563
|
+
* already refuses rather than silently running unconstrained; this only reads the shape. */
|
|
564
|
+
function readResourceLimits(args) {
|
|
565
|
+
const raw = args["resourceLimits"];
|
|
566
|
+
if (raw === undefined || raw === null)
|
|
567
|
+
return undefined;
|
|
568
|
+
const record = asRecord(raw, "resourceLimits");
|
|
569
|
+
const maxMemoryBytes = optionalInteger(record, "maxMemoryBytes", 1);
|
|
570
|
+
const maxCpuSeconds = optionalInteger(record, "maxCpuSeconds", 1);
|
|
571
|
+
const maxProcesses = optionalInteger(record, "maxProcesses", 1);
|
|
572
|
+
return {
|
|
573
|
+
...(maxMemoryBytes === undefined ? {} : { maxMemoryBytes }),
|
|
574
|
+
...(maxCpuSeconds === undefined ? {} : { maxCpuSeconds }),
|
|
575
|
+
...(maxProcesses === undefined ? {} : { maxProcesses }),
|
|
576
|
+
};
|
|
577
|
+
}
|
|
578
|
+
/** A-F5: FilesystemPolicy. allowedRoots is required whenever filesystemPolicy is present —
|
|
579
|
+
* an empty-roots policy silently means "cwd only", and that must be stated, not defaulted into. */
|
|
580
|
+
function readFilesystemPolicy(args) {
|
|
581
|
+
const raw = args["filesystemPolicy"];
|
|
582
|
+
if (raw === undefined || raw === null)
|
|
583
|
+
return undefined;
|
|
584
|
+
const record = asRecord(raw, "filesystemPolicy");
|
|
585
|
+
const allowedRoots = record["allowedRoots"];
|
|
586
|
+
if (!Array.isArray(allowedRoots) || allowedRoots.some((r) => typeof r !== "string")) {
|
|
587
|
+
throw new ToolInputError('"filesystemPolicy.allowedRoots" must be an array of strings');
|
|
588
|
+
}
|
|
589
|
+
return { allowedRoots: allowedRoots };
|
|
590
|
+
}
|
|
591
|
+
/** A-F5: NetworkPolicy. Only `{mode:"allow", hosts:[]}` (full denial) is actually enforced
|
|
592
|
+
* by applySandbox today; any other shape is accepted here and refused downstream by the
|
|
593
|
+
* mechanism itself (execution.validationError), never pre-judged here (rule 1). */
|
|
594
|
+
function readNetworkPolicy(args) {
|
|
595
|
+
const raw = args["networkPolicy"];
|
|
596
|
+
if (raw === undefined || raw === null)
|
|
597
|
+
return undefined;
|
|
598
|
+
const record = asRecord(raw, "networkPolicy");
|
|
599
|
+
const mode = optionalEnum(record, "mode", ["allow", "deny"]);
|
|
600
|
+
if (mode === undefined)
|
|
601
|
+
throw new ToolInputError('"networkPolicy.mode" is required');
|
|
602
|
+
const hosts = record["hosts"];
|
|
603
|
+
if (!Array.isArray(hosts) || hosts.some((h) => typeof h !== "string")) {
|
|
604
|
+
throw new ToolInputError('"networkPolicy.hosts" must be an array of strings');
|
|
605
|
+
}
|
|
606
|
+
return { mode, hosts: hosts };
|
|
607
|
+
}
|
|
608
|
+
function readSandboxBackend(args) {
|
|
609
|
+
return optionalEnum(args, "sandboxBackend", ["bwrap", "container"]);
|
|
610
|
+
}
|
|
679
611
|
export function readArguments(args, repoPath) {
|
|
680
612
|
const profile = readProfile(args);
|
|
681
613
|
const adapterSpec = readAdapterSpec(args);
|
|
@@ -687,21 +619,46 @@ export function readArguments(args, repoPath) {
|
|
|
687
619
|
throw new ToolInputError('"fidelityLevel" must be 1, 2, 3 or 4');
|
|
688
620
|
const environmentTier = optionalString(args, "environmentTier") ?? "tier-2-container";
|
|
689
621
|
const evidenceArg = optionalString(args, "evidencePath");
|
|
690
|
-
const
|
|
622
|
+
const evidenceCandidate = evidenceArg === undefined
|
|
691
623
|
? join(repoPath, DEFAULT_EVIDENCE_RELATIVE_PATH)
|
|
692
624
|
: isAbsolute(evidenceArg)
|
|
693
625
|
? evidenceArg
|
|
694
626
|
: join(repoPath, evidenceArg);
|
|
695
|
-
|
|
627
|
+
// A-F6: join() doesn't stop ".." escaping repoPath — confined by default (path-confinement.ts).
|
|
628
|
+
const confinement = checkPathConfinement({
|
|
629
|
+
candidate: evidenceCandidate,
|
|
630
|
+
allowedRoots: [repoPath],
|
|
631
|
+
envVar: ALLOW_EVIDENCE_PATH_OUTSIDE_REPO_ENV_VAR,
|
|
632
|
+
what: "evidencePath",
|
|
633
|
+
});
|
|
634
|
+
if (!confinement.allowed)
|
|
635
|
+
throw new ToolInputError(confinement.reason);
|
|
636
|
+
const evidencePath = confinement.resolved;
|
|
637
|
+
const resourceLimits = readResourceLimits(args);
|
|
638
|
+
const filesystemPolicy = readFilesystemPolicy(args);
|
|
639
|
+
const networkPolicy = readNetworkPolicy(args);
|
|
640
|
+
const sandboxBackend = readSandboxBackend(args);
|
|
641
|
+
return {
|
|
642
|
+
profile,
|
|
643
|
+
adapterSpec,
|
|
644
|
+
declared,
|
|
645
|
+
observeForMs,
|
|
646
|
+
timeoutMs,
|
|
647
|
+
fidelityRaw,
|
|
648
|
+
environmentTier,
|
|
649
|
+
evidencePath,
|
|
650
|
+
resourceLimits,
|
|
651
|
+
filesystemPolicy,
|
|
652
|
+
networkPolicy,
|
|
653
|
+
sandboxBackend,
|
|
654
|
+
};
|
|
696
655
|
}
|
|
697
656
|
async function run(args, ctx) {
|
|
698
657
|
const session = ctx.session;
|
|
699
|
-
const { profile, adapterSpec, declared, observeForMs, timeoutMs, fidelityRaw, environmentTier, evidencePath } = readArguments(args, session.repoPath);
|
|
700
|
-
const notes = [...STANDING_NOTES];
|
|
701
|
-
// Added on
|
|
702
|
-
//
|
|
703
|
-
// again; telling them about the buffering only on the success path means
|
|
704
|
-
// telling them after the run whose result it would have explained.
|
|
658
|
+
const { profile, adapterSpec, declared, observeForMs, timeoutMs, fidelityRaw, environmentTier, evidencePath, resourceLimits, filesystemPolicy, networkPolicy, sandboxBackend, } = readArguments(args, session.repoPath);
|
|
659
|
+
const notes = [...STANDING_NOTES, sandboxDisclosure({ resourceLimits, filesystemPolicy, networkPolicy, sandboxBackend })];
|
|
660
|
+
// Added on refusal paths too — telling the caller only on success means telling them
|
|
661
|
+
// after the run whose result it would have explained.
|
|
705
662
|
if (declared.some((service) => service.attached))
|
|
706
663
|
notes.push(ATTACH_BUFFERING_NOTE);
|
|
707
664
|
const base = session.provider().baseStamp();
|
|
@@ -709,9 +666,7 @@ async function run(args, ctx) {
|
|
|
709
666
|
headline,
|
|
710
667
|
state: "refused",
|
|
711
668
|
nameLevel: true,
|
|
712
|
-
// Nothing ran, so nothing was resolved
|
|
713
|
-
// successful path would have reached is the leaked-default this repo's
|
|
714
|
-
// own UAT already caught once elsewhere.
|
|
669
|
+
// Nothing ran, so nothing was resolved — never leak the tier a successful path would reach.
|
|
715
670
|
resolutionFloor: 0,
|
|
716
671
|
commitSha: base.commitSha,
|
|
717
672
|
graphBuiltAt: base.graphBuiltAt,
|
|
@@ -730,11 +685,8 @@ async function run(args, ctx) {
|
|
|
730
685
|
...data,
|
|
731
686
|
},
|
|
732
687
|
});
|
|
733
|
-
//
|
|
734
|
-
//
|
|
735
|
-
// executes code in, signals, or applies limits to a process it did not
|
|
736
|
-
// spawn). So the action's shape depends on what was declared, and the
|
|
737
|
-
// profile's declared level decides — never the profile's name.
|
|
688
|
+
// Spawning is a write against the target; attaching is not (Descry never executes code in,
|
|
689
|
+
// signals, or limits a process it didn't spawn). The profile's declared level decides, never its name.
|
|
738
690
|
const spawns = declared.some((service) => !service.attached);
|
|
739
691
|
const action = { write: spawns, destructive: false };
|
|
740
692
|
const decision = evaluateAction(profile, action);
|
|
@@ -750,10 +702,8 @@ async function run(args, ctx) {
|
|
|
750
702
|
"applied no resource, filesystem or network policy to any process — Descry does not constrain " +
|
|
751
703
|
"a process it did not spawn.");
|
|
752
704
|
}
|
|
753
|
-
//
|
|
754
|
-
//
|
|
755
|
-
// resolves nothing, and reporting that as a clean run with no findings would
|
|
756
|
-
// be the exact "empty means broken" collapse the five states exist to stop.
|
|
705
|
+
// Correlation resolves evidence against this graph; an empty graph resolving nothing must
|
|
706
|
+
// not be reported as a clean run with no findings — the "empty means broken" collapse.
|
|
757
707
|
const driver = session.store().driver;
|
|
758
708
|
const stored = counts(driver);
|
|
759
709
|
if (stored.nodes === 0) {
|
|
@@ -764,7 +714,7 @@ async function run(args, ctx) {
|
|
|
764
714
|
ctx.progress(`Loading runtime adapter ${adapterSpec.module}`);
|
|
765
715
|
let adapter;
|
|
766
716
|
try {
|
|
767
|
-
adapter = await loadRuntimeAdapter(adapterSpec);
|
|
717
|
+
adapter = await loadRuntimeAdapter(adapterSpec, session.repoPath);
|
|
768
718
|
}
|
|
769
719
|
catch (error) {
|
|
770
720
|
if (error instanceof RuntimeAdapterLoadError) {
|
|
@@ -791,25 +741,23 @@ async function run(args, ctx) {
|
|
|
791
741
|
fidelityLevel: fidelityRaw,
|
|
792
742
|
timeoutMs,
|
|
793
743
|
services,
|
|
744
|
+
// A-F5: wired through to the controller's real bwrap/container isolation
|
|
745
|
+
// — see sandboxDisclosure() above for what this reply says about it.
|
|
746
|
+
...(resourceLimits === undefined ? {} : { resourceLimits }),
|
|
747
|
+
...(filesystemPolicy === undefined ? {} : { filesystemPolicy }),
|
|
748
|
+
...(networkPolicy === undefined ? {} : { networkPolicy }),
|
|
749
|
+
...(sandboxBackend === undefined ? {} : { sandboxBackend }),
|
|
794
750
|
};
|
|
795
|
-
// Before anything is spawned
|
|
796
|
-
//
|
|
797
|
-
// nothing left that would ever stop it.
|
|
751
|
+
// Before anything is spawned — a caller who's already gone would leave a real process
|
|
752
|
+
// running with nothing left to ever stop it.
|
|
798
753
|
if (ctx.signal.aborted) {
|
|
799
754
|
return refuse(cancellationHeadline("no service was started") + " " + cancellationNotes("Nothing was spawned.")[0]);
|
|
800
755
|
}
|
|
801
756
|
await mkdir(dirname(evidencePath), { recursive: true });
|
|
802
757
|
const evidenceStore = new EvidenceStore({ path: evidencePath });
|
|
803
|
-
/**
|
|
804
|
-
*
|
|
805
|
-
*
|
|
806
|
-
* An `attach`-mode service was already running before this tool was called —
|
|
807
|
-
* it is the developer's own process, borrowed for the length of an
|
|
808
|
-
* observation. Killing it on a cancellation would destroy something this
|
|
809
|
-
* call never created, which is a worse failure than the leak being fixed.
|
|
810
|
-
* The spawn path is the one that is cleaned up, because it is the one this
|
|
811
|
-
* call is responsible for.
|
|
812
|
-
*/
|
|
758
|
+
/** Processes **this run started**, and only those — an attach-mode service is the
|
|
759
|
+
* developer's own process; killing it on cancellation would destroy something this
|
|
760
|
+
* call never created. Only the spawn path is cleaned up. */
|
|
813
761
|
const attached = new Set(declared.filter((service) => service.attached).map((service) => service.name));
|
|
814
762
|
const spawnedPids = new Set();
|
|
815
763
|
const abort = whenAborted(ctx.signal);
|
|
@@ -828,10 +776,8 @@ async function run(args, ctx) {
|
|
|
828
776
|
},
|
|
829
777
|
runOptions: {
|
|
830
778
|
readiness,
|
|
831
|
-
// The only channel
|
|
832
|
-
//
|
|
833
|
-
// resolves, and by then the whole observation window has already been
|
|
834
|
-
// slept — which is exactly the stretch a cancellation lands in.
|
|
779
|
+
// The only channel naming a process while still alive — Execution.processes is
|
|
780
|
+
// complete only once the whole window has already slept, past where a cancel lands.
|
|
835
781
|
onProcessLifecycleEvent: (event) => {
|
|
836
782
|
if (event.kind !== "process-started" || attached.has(event.serviceName))
|
|
837
783
|
return;
|
|
@@ -843,12 +789,9 @@ async function run(args, ctx) {
|
|
|
843
789
|
store: evidenceStore,
|
|
844
790
|
observeForMs,
|
|
845
791
|
});
|
|
846
|
-
// The
|
|
847
|
-
//
|
|
848
|
-
//
|
|
849
|
-
// one claim in this system that cannot be re-derived from source and
|
|
850
|
-
// checked later, and minting one from a run nobody watched to the end
|
|
851
|
-
// would put a fact into the graph that no later pass could question.
|
|
792
|
+
// The kill above doesn't shorten the window — this is where a cancelled run actually
|
|
793
|
+
// stops. Nothing is correlated or written: an R4 edge can't be re-derived and checked
|
|
794
|
+
// later, so minting one from a run nobody watched to the end would be unquestionable.
|
|
852
795
|
if (ctx.signal.aborted) {
|
|
853
796
|
return refuse(cancellationHeadline("the observed application was stopped"), {
|
|
854
797
|
executionId: execution.execution.executionId,
|
|
@@ -863,9 +806,8 @@ async function run(args, ctx) {
|
|
|
863
806
|
const observed = describeServices(execution.execution.processes, declared);
|
|
864
807
|
const evidenceByType = tally(execution.evidence);
|
|
865
808
|
if (execution.validationError !== null) {
|
|
866
|
-
//
|
|
867
|
-
//
|
|
868
|
-
// correlation, and emphatically not "the service is clean".
|
|
809
|
+
// Refused before spawning — a fact about the declaration, not the application;
|
|
810
|
+
// emphatically not "the service is clean".
|
|
869
811
|
return refuse(`The execution refused to start: ${execution.validationError}. Nothing was spawned, no ` +
|
|
870
812
|
"evidence was collected, and no graph edge was written.", {
|
|
871
813
|
executionState: execution.execution.state,
|
|
@@ -874,24 +816,9 @@ async function run(args, ctx) {
|
|
|
874
816
|
evidenceByType,
|
|
875
817
|
});
|
|
876
818
|
}
|
|
877
|
-
//
|
|
878
|
-
//
|
|
879
|
-
//
|
|
880
|
-
// arms a timer for it — that timer kills the services and nothing else.
|
|
881
|
-
// Measured: an 8s budget on a live attach returned after ~90s, and a
|
|
882
|
-
// 1.5s budget over a 12s window returned after 12.3s with zero evidence,
|
|
883
|
-
// because the processes had been dead for the last 10.8s of a window
|
|
884
|
-
// nothing shortened (`DEC-NEXT-observe-runtime-timeout-budget-is-not-
|
|
885
|
-
// enforced.md`). The window is now clamped in the orchestrator; this is
|
|
886
|
-
// the other half — saying so.
|
|
887
|
-
//
|
|
888
|
-
// Reported as `timed_out` rather than `ok`. §6 calls that state "not a
|
|
889
|
-
// silent truncation and never a `failed`", which is exactly this run: the
|
|
890
|
-
// evidence below is real and the R4 writes below it are honest, there is
|
|
891
|
-
// simply less of both than was asked for. `ok` would make "we watched the
|
|
892
|
-
// whole window and it was quiet" indistinguishable from "we stopped
|
|
893
|
-
// watching a tenth of the way in", and a caller acting on the first
|
|
894
|
-
// concludes the application is fine.
|
|
819
|
+
// Budget expired before the window closed. Measured pre-clamp: a 1.5s budget over a 12s
|
|
820
|
+
// window returned at 12.3s with zero evidence (DEC-NEXT-observe-runtime-timeout-budget-
|
|
821
|
+
// is-not-enforced.md). Reported `timed_out`, not `ok` (§6) — real evidence, just less of it.
|
|
895
822
|
const timedOut = execution.execution.state === "TIMED_OUT";
|
|
896
823
|
if (timedOut) {
|
|
897
824
|
notes.push(`This run hit its ${String(timeoutMs)}ms whole-execution budget before the ` +
|
|
@@ -916,16 +843,8 @@ async function run(args, ctx) {
|
|
|
916
843
|
harnessErrors: pass.harnessErrors.map((e) => `${e.detail} (${String(e.occurrences)}×)`),
|
|
917
844
|
};
|
|
918
845
|
if (pass.unscopedServices.length > 0) {
|
|
919
|
-
// Two
|
|
920
|
-
//
|
|
921
|
-
// scope is something the caller can fix by supplying one. The `(no
|
|
922
|
-
// service)` sentinel is not: `correlateExecution` looks a scope up by
|
|
923
|
-
// `evidence.service`, and no collector in this dependency closure stamps
|
|
924
|
-
// one — measured by the conformance run, which prints a real V8 stack
|
|
925
|
-
// resolving to a real graph node and watches it go unasked. Telling a
|
|
926
|
-
// caller to name a scope they have no key for would send them after a
|
|
927
|
-
// fix that does not exist, which is the honest-degradation rule failing
|
|
928
|
-
// in the one place it acts.
|
|
846
|
+
// Two sentences, not one: a named service with no scope is fixable by the caller; the
|
|
847
|
+
// "(no service)" sentinel is not — no collector stamps one, so there's no key to supply.
|
|
929
848
|
const named = pass.unscopedServices.filter((s) => s !== "(no service)");
|
|
930
849
|
const anonymous = pass.unscopedServices.length - named.length;
|
|
931
850
|
if (named.length > 0) {
|
|
@@ -948,10 +867,8 @@ async function run(args, ctx) {
|
|
|
948
867
|
"than a resolver honestly declining — each is written into the evidence stream as a " +
|
|
949
868
|
"COLLECTOR_ERROR, and the counts below are correspondingly incomplete.");
|
|
950
869
|
}
|
|
951
|
-
//
|
|
952
|
-
//
|
|
953
|
-
// failure in one does not silently cost the other.
|
|
954
|
-
// See `runtime-incident.ts` for why an EXCEPTION alone is not an incident.
|
|
870
|
+
// Recorded before the edge write so a failure in one doesn't silently cost the other.
|
|
871
|
+
// See runtime-incident.ts for why an EXCEPTION alone is not an incident.
|
|
955
872
|
const incident = runtimeObservedIncident({
|
|
956
873
|
repo: root.repo,
|
|
957
874
|
repoRoot: root.absolutePath,
|
|
@@ -1004,33 +921,17 @@ async function run(args, ctx) {
|
|
|
1004
921
|
`${String(wrote.promoted.length)} edge(s) promoted to R4 and ${String(wrote.created.length)} minted at R4.`;
|
|
1005
922
|
return answer({
|
|
1006
923
|
headline,
|
|
1007
|
-
//
|
|
1008
|
-
//
|
|
1009
|
-
//
|
|
1010
|
-
//
|
|
1011
|
-
// `timed_out` when the budget bound this run — see the block above. It
|
|
1012
|
-
// is the same distinction one step further out: `empty` and `ok` both
|
|
1013
|
-
// claim the window was watched to its end.
|
|
924
|
+
// Not `empty` when nothing was witnessed — `empty` claims the population, and "this run
|
|
925
|
+
// took no path exercising the code" isn't "this code does nothing". `timed_out` when the
|
|
926
|
+
// budget bound this run (see above): `empty`/`ok` both claim the window ran to its end.
|
|
1014
927
|
state: timedOut ? "timed_out" : "ok",
|
|
1015
928
|
nameLevel: true,
|
|
1016
|
-
// R4 unconditionally
|
|
1017
|
-
//
|
|
1018
|
-
// rest on inference (DEC-115). The correlated-but-unwritten items are
|
|
1019
|
-
// not claimed here at all; they resolved a node and asserted nothing, so
|
|
1020
|
-
// a run that wrote nothing reports R0 rather than borrowing the tier its
|
|
1021
|
-
// successful path would have reached.
|
|
929
|
+
// R4 unconditionally: every `wrote` fact was witnessed at runtime (DEC-115, no inference).
|
|
930
|
+
// A run that wrote nothing reports R0 rather than borrowing the tier a success would reach.
|
|
1022
931
|
resolutionFloor: (written > 0 ? 4 : 0),
|
|
1023
|
-
// G3
|
|
1024
|
-
//
|
|
1025
|
-
//
|
|
1026
|
-
// otherwise would assert a precondition on an empty array.
|
|
1027
|
-
//
|
|
1028
|
-
// Note this is deliberately *not* gated on `written > 0`. Whether an
|
|
1029
|
-
// edge could be written is a fact about the graph path, and rule 3
|
|
1030
|
-
// already caps the category through `resolutionFloor` just above — a
|
|
1031
|
-
// run that saw three channels and wrote no edge is reported
|
|
1032
|
-
// `unconfirmed` by the cap, not by pretending it saw nothing. Two
|
|
1033
|
-
// separate facts, each stated once.
|
|
932
|
+
// G3/G4's E, declared only when evidence actually came back — not gated on `written > 0`:
|
|
933
|
+
// rule 3 already caps the category via resolutionFloor above, so a run that saw channels
|
|
934
|
+
// but wrote no edge is `unconfirmed` by that cap, not by pretending it saw nothing.
|
|
1034
935
|
...(execution.evidence.length === 0
|
|
1035
936
|
? {}
|
|
1036
937
|
: { runtimeEvidence: { independentSignalTypes: witnessedSignalTypes(execution.evidence) } }),
|
|
@@ -1056,16 +957,8 @@ async function run(args, ctx) {
|
|
|
1056
957
|
evidenceStore.close();
|
|
1057
958
|
}
|
|
1058
959
|
}
|
|
1059
|
-
/**
|
|
1060
|
-
*
|
|
1061
|
-
*
|
|
1062
|
-
* The controller spawns detached specifically so a service that forks — a dev
|
|
1063
|
-
* server that runs a compiler, a runtime that supervises a worker — can be
|
|
1064
|
-
* stopped whole. Signalling the leader alone would reap the parent and leave
|
|
1065
|
-
* its children holding the port, which reads as a successful cleanup and is
|
|
1066
|
-
* not one. `ESRCH` is the ordinary case, not an error: the process may have
|
|
1067
|
-
* exited on its own between the abort and this call.
|
|
1068
|
-
*/
|
|
960
|
+
/** Stop a process this run spawned, and the group it leads — signalling the leader alone
|
|
961
|
+
* would leave forked children holding the port. ESRCH is ordinary: it may have already exited. */
|
|
1069
962
|
function terminateSpawnedProcess(pid) {
|
|
1070
963
|
try {
|
|
1071
964
|
process.kill(-pid, "SIGTERM");
|
|
@@ -1082,12 +975,8 @@ function terminateSpawnedProcess(pid) {
|
|
|
1082
975
|
// Already gone. Nothing to report and nothing to do.
|
|
1083
976
|
}
|
|
1084
977
|
}
|
|
1085
|
-
/**
|
|
1086
|
-
*
|
|
1087
|
-
* that never started must appear with `started: false` rather than vanish from
|
|
1088
|
-
* the list, which is the difference between "it ran and did nothing" and "it
|
|
1089
|
-
* never ran".
|
|
1090
|
-
*/
|
|
978
|
+
/** One row per **declared** service, not per spawned process — a never-started service
|
|
979
|
+
* appears with `started: false` rather than vanishing ("it did nothing" vs "it never ran"). */
|
|
1091
980
|
function describeServices(processes, declared) {
|
|
1092
981
|
const byService = new Map();
|
|
1093
982
|
for (const handle of processes) {
|
|
@@ -1113,32 +1002,9 @@ function tally(evidence) {
|
|
|
1113
1002
|
byType[item.eventType] = (byType[item.eventType] ?? 0) + 1;
|
|
1114
1003
|
return byType;
|
|
1115
1004
|
}
|
|
1116
|
-
/**
|
|
1117
|
-
*
|
|
1118
|
-
*
|
|
1119
|
-
*
|
|
1120
|
-
* **Decided by `eventType`, with `source` consulted only where the event type
|
|
1121
|
-
* is genuinely ambiguous** — an exception can come off a browser console or a
|
|
1122
|
-
* backend process, and nothing but the collector says which. The obvious
|
|
1123
|
-
* alternative, reading `source` alone, is wrong and was measured to be wrong
|
|
1124
|
-
* rather than reasoned about: every row this repository's own conformance run
|
|
1125
|
-
* produces carries `source: "backend-process"`, and `EVIDENCE_SOURCES` also
|
|
1126
|
-
* has a `backend-log` value that nothing in the shipped dependency closure
|
|
1127
|
-
* emits. A source-driven table would have counted zero channels on every real
|
|
1128
|
-
* run while passing a hand-built test — the exact shape of failure that gets
|
|
1129
|
-
* caught by running the thing.
|
|
1130
|
-
*
|
|
1131
|
-
* **Everything not listed returns `null` and is counted as nothing.** That is
|
|
1132
|
-
* `RuntimeEvidence`'s own rule, not caution added here: `TEST_*` is excluded
|
|
1133
|
-
* by design (its evidentiary weight is `M`'s, never double-counted as a
|
|
1134
|
-
* channel too), harness actions are Descry driving the application rather than
|
|
1135
|
-
* observing it, process lifecycle is a fact about the process rather than
|
|
1136
|
-
* about its behaviour, and a collector or version-mismatch error is a fact
|
|
1137
|
-
* about the run. A kind this table has not ruled on must fail closed, because
|
|
1138
|
-
* the alternative — mapping it to the nearest-looking channel — raises `E`,
|
|
1139
|
-
* and therefore the reported category, with nobody having decided that it
|
|
1140
|
-
* should.
|
|
1141
|
-
*/
|
|
1005
|
+
/** One evidence row onto one of @descryy/ir's six RUNTIME_SIGNAL_TYPES, or null. Decided by
|
|
1006
|
+
* `eventType`; `source` consulted only for the ambiguous EXCEPTION/STACK_TRACE pair (reading
|
|
1007
|
+
* `source` alone was measured wrong). Unlisted kinds fail closed to null, never guess-mapped. */
|
|
1142
1008
|
function signalOf(item) {
|
|
1143
1009
|
switch (item.eventType) {
|
|
1144
1010
|
case "CONSOLE_MESSAGE":
|
|
@@ -1157,9 +1023,7 @@ function signalOf(item) {
|
|
|
1157
1023
|
return "database";
|
|
1158
1024
|
case "EXTERNAL_REQUEST":
|
|
1159
1025
|
return "external-service";
|
|
1160
|
-
// The ambiguous pair
|
|
1161
|
-
// reaches Descry through whichever collector saw it, and that collector is
|
|
1162
|
-
// the channel.
|
|
1026
|
+
// The ambiguous pair: a thrown error reaches Descry through whichever collector saw it.
|
|
1163
1027
|
case "EXCEPTION":
|
|
1164
1028
|
case "STACK_TRACE":
|
|
1165
1029
|
return item.source === "browser-console" ? "browser-console" : "backend-log";
|
|
@@ -1167,31 +1031,15 @@ function signalOf(item) {
|
|
|
1167
1031
|
return null;
|
|
1168
1032
|
}
|
|
1169
1033
|
}
|
|
1170
|
-
/**
|
|
1171
|
-
*
|
|
1172
|
-
*
|
|
1173
|
-
* The count itself is `@descryy/ir`'s `independentSignalTypes`, deliberately:
|
|
1174
|
-
* the clamp to six and the drop of `null` signals are that function's rules,
|
|
1175
|
-
* and a second implementation of them here is a second place for the
|
|
1176
|
-
* vocabulary to drift. This function's only job is the translation above.
|
|
1177
|
-
*
|
|
1178
|
-
* Exported for its own test — the mapping decides whether a witnessed answer
|
|
1179
|
-
* reads `strongly supported` or `unconfirmed`, which is too load-bearing to be
|
|
1180
|
-
* asserted only through a category two layers downstream.
|
|
1181
|
-
*/
|
|
1034
|
+
/** `E` for this run — channels actually seen, via @descryy/ir's independentSignalTypes
|
|
1035
|
+
* (clamp-to-six and null-drop are that function's rules, not duplicated here). Exported for
|
|
1036
|
+
* its own test — this mapping decides `strongly supported` vs `unconfirmed`. */
|
|
1182
1037
|
export function witnessedSignalTypes(evidence) {
|
|
1183
1038
|
return independentSignalTypes(evidence.map((item) => ({ signal: signalOf(item), detail: item.eventType })));
|
|
1184
1039
|
}
|
|
1185
|
-
/**
|
|
1186
|
-
*
|
|
1187
|
-
*
|
|
1188
|
-
* For every evidence item the correlation pass resolved to an endpoint, if that
|
|
1189
|
-
* same item also carried a call-site stack, ask `confirmObservedFrontendCaller`
|
|
1190
|
-
* whether the stack names a function — and when it does, it writes. Both
|
|
1191
|
-
* endpoints of the resulting edge come from the one observation; nothing here
|
|
1192
|
-
* pairs two separate items together. See the module header for why that
|
|
1193
|
-
* restraint is the whole design rather than a limitation of it.
|
|
1194
|
-
*/
|
|
1040
|
+
/** The R4 write, and the one join this tool performs: for every evidence item resolved to an
|
|
1041
|
+
* endpoint that also carries a call-site stack, ask confirmObservedFrontendCaller. Both
|
|
1042
|
+
* endpoints come from one observation — nothing here pairs two separate items (see header). */
|
|
1195
1043
|
function writeObservations(input) {
|
|
1196
1044
|
const promoted = [];
|
|
1197
1045
|
const created = [];
|
|
@@ -1199,19 +1047,13 @@ function writeObservations(input) {
|
|
|
1199
1047
|
const refused = [];
|
|
1200
1048
|
const contradictions = [];
|
|
1201
1049
|
const staleR4 = [];
|
|
1202
|
-
// One evidence item can be attributed twice
|
|
1203
|
-
//
|
|
1204
|
-
// is never confirmed twice within one run.
|
|
1050
|
+
// One evidence item can be attributed twice; keyed on both so the same (evidence,
|
|
1051
|
+
// endpoint) pair is never confirmed twice within one run.
|
|
1205
1052
|
const seen = new Set();
|
|
1206
1053
|
for (const attribution of input.pass.attributed) {
|
|
1207
|
-
//
|
|
1208
|
-
//
|
|
1209
|
-
//
|
|
1210
|
-
// endpoint, not that it called it. The commonest real shape is a handler
|
|
1211
|
-
// logging "GET /invoices -> 500", and that function SERVES the endpoint
|
|
1212
|
-
// rather than USING it, so an edge minted from it could point the wrong
|
|
1213
|
-
// way. Rule 2: a wrong edge corrupts diff scoping, impact scores and
|
|
1214
|
-
// root-cause traversal; a missing one is a disclosed gap. Omitted.
|
|
1054
|
+
// "endpoint" only — "log-text-endpoint" fires when a log line mentions a route, which
|
|
1055
|
+
// means the function SERVES it, not calls it; minting from that could point the wrong
|
|
1056
|
+
// way. Rule 2: a wrong edge is worse than a missing (disclosed) one. Omitted.
|
|
1215
1057
|
if (attribution.family !== "endpoint")
|
|
1216
1058
|
continue;
|
|
1217
1059
|
const key = `${attribution.evidenceId}::${attribution.graphNodeId}`;
|
|
@@ -1238,29 +1080,9 @@ function writeObservations(input) {
|
|
|
1238
1080
|
}
|
|
1239
1081
|
return { promoted, created, confirmed, refused, contradictions, staleR4 };
|
|
1240
1082
|
}
|
|
1241
|
-
/**
|
|
1242
|
-
*
|
|
1243
|
-
*
|
|
1244
|
-
* Always returns a sentence — unlike `questions`, whose unconfirmed shape is a
|
|
1245
|
-
* genuine pure read of the question queue, there is no argument to this tool
|
|
1246
|
-
* that makes it not run anything. Every valid call spawns or attaches, and
|
|
1247
|
-
* every one of them can write.
|
|
1248
|
-
*/
|
|
1249
|
-
/**
|
|
1250
|
-
* §7's `willDo`, and — since `readArguments` is the first thing it does — the
|
|
1251
|
-
* point where an invalid call is refused.
|
|
1252
|
-
*
|
|
1253
|
-
* Returns a string on every valid call rather than ever returning `undefined`:
|
|
1254
|
-
* `undefined` means "this particular call has nothing to confirm", and there
|
|
1255
|
-
* is no such call here. Every accepted declaration boots or attaches to
|
|
1256
|
-
* something and may write durable R4 facts.
|
|
1257
|
-
*
|
|
1258
|
-
* Reads the *parsed* services rather than the raw object, so the sentence a
|
|
1259
|
-
* developer confirms is built from the same values the run will use — which
|
|
1260
|
-
* is also what makes "spawn" and "attach" here mean exactly what
|
|
1261
|
-
* `readServices` decided they mean, rather than a second, looser guess at the
|
|
1262
|
-
* same distinction.
|
|
1263
|
-
*/
|
|
1083
|
+
/** §7's `willDo` — and, since readArguments runs first, where an invalid call is refused.
|
|
1084
|
+
* Always returns a string, never undefined: unlike `questions`, no call here has nothing
|
|
1085
|
+
* to confirm. Reads the *parsed* services so the confirmation sentence matches the run exactly. */
|
|
1264
1086
|
function describeAction(args, ctx) {
|
|
1265
1087
|
const { declared } = readArguments(args, ctx.session.repoPath);
|
|
1266
1088
|
const spawned = declared.filter((service) => !service.attached).map((service) => service.name);
|