@deftai/directive-core 0.107.0 → 0.109.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/architecture/sor-preflight.js +2 -0
- package/dist/cache/operations.js +1 -1
- package/dist/check/cached-orchestrator.js +28 -3
- package/dist/check/gate-lists.d.ts +14 -0
- package/dist/check/gate-lists.js +26 -3
- package/dist/check/named-cause.js +18 -3
- package/dist/check/session-completed-ac.d.ts +1 -1
- package/dist/check/session-completed-ac.js +1 -1
- package/dist/codebase/provider.js +7 -1
- package/dist/consumer-check-contract/evaluate.d.ts +47 -0
- package/dist/consumer-check-contract/evaluate.js +181 -15
- package/dist/delivery-attempt/evaluate.d.ts +9 -1
- package/dist/delivery-attempt/evaluate.js +69 -0
- package/dist/delivery-attempt/handoff.js +1 -1
- package/dist/delivery-attempt/index.d.ts +1 -1
- package/dist/delivery-attempt/index.js +1 -1
- package/dist/deposit/live-procedure-exclusions.d.ts +18 -0
- package/dist/deposit/live-procedure-exclusions.js +110 -0
- package/dist/deposit/live-procedure-targets.d.ts +45 -0
- package/dist/deposit/live-procedure-targets.js +274 -0
- package/dist/deposit/python-free.d.ts +6 -0
- package/dist/deposit/python-free.js +15 -0
- package/dist/deposit/rewrite-deposit-links.d.ts +42 -0
- package/dist/deposit/rewrite-deposit-links.js +148 -0
- package/dist/deposit/run-stage-content-pack.d.ts +2 -0
- package/dist/deposit/run-stage-content-pack.js +3 -0
- package/dist/deposit/stage-content-pack.d.ts +17 -0
- package/dist/deposit/stage-content-pack.js +91 -0
- package/dist/design-critique/citation-grammar.d.ts +60 -0
- package/dist/design-critique/citation-grammar.js +303 -0
- package/dist/design-critique/completed-arc-record.d.ts +62 -0
- package/dist/design-critique/completed-arc-record.js +277 -0
- package/dist/freshness/bind.d.ts +32 -3
- package/dist/freshness/bind.js +58 -12
- package/dist/hooks/classify/host-session-identity.d.ts +89 -0
- package/dist/hooks/classify/host-session-identity.js +467 -0
- package/dist/hooks/classify/index.d.ts +3 -2
- package/dist/hooks/classify/index.js +3 -2
- package/dist/hooks/classify/paths.d.ts +9 -0
- package/dist/hooks/classify/paths.js +39 -0
- package/dist/hooks/classify/stdin.d.ts +5 -0
- package/dist/hooks/classify/stdin.js +94 -1
- package/dist/hooks/dest-form.d.ts +20 -1
- package/dist/hooks/dest-form.js +158 -21
- package/dist/hooks/dispatcher.d.ts +58 -3
- package/dist/hooks/dispatcher.js +762 -73
- package/dist/hooks/fixtures/cases.d.ts +22 -2
- package/dist/hooks/fixtures/cases.js +228 -0
- package/dist/hooks/git-destructive-log.d.ts +32 -0
- package/dist/hooks/git-destructive-log.js +46 -0
- package/dist/hooks/index.d.ts +2 -0
- package/dist/hooks/index.js +2 -0
- package/dist/hooks/scope.js +3 -1
- package/dist/hooks/shell-write-targets.d.ts +10 -0
- package/dist/hooks/shell-write-targets.js +274 -0
- package/dist/hooks/tools.d.ts +20 -2
- package/dist/hooks/tools.js +30 -1
- package/dist/init-deposit/agent-hooks.d.ts +2 -1
- package/dist/init-deposit/agent-hooks.js +8 -1
- package/dist/init-deposit/gitignore.d.ts +7 -0
- package/dist/init-deposit/gitignore.js +24 -0
- package/dist/init-deposit/hook-runtime-travel.d.ts +77 -0
- package/dist/init-deposit/hook-runtime-travel.js +269 -0
- package/dist/init-deposit/init-deposit.js +3 -0
- package/dist/init-deposit/refresh.js +7 -0
- package/dist/init-deposit/runtime-writers.d.ts +14 -0
- package/dist/init-deposit/runtime-writers.js +33 -0
- package/dist/intake/clause-derivation.js +9 -3
- package/dist/intake/issue-ingest.d.ts +6 -1
- package/dist/intake/issue-ingest.js +83 -4
- package/dist/intake/platform-capabilities.d.ts +9 -2
- package/dist/intake/platform-capabilities.js +38 -9
- package/dist/intake/reconcile-issues.js +13 -13
- package/dist/lifecycle/brief-envelope.d.ts +25 -0
- package/dist/lifecycle/brief-envelope.js +42 -0
- package/dist/lifecycle/index.d.ts +1 -0
- package/dist/lifecycle/index.js +1 -0
- package/dist/literal-acceptance/evaluate.js +14 -5
- package/dist/literal-acceptance/index.d.ts +1 -1
- package/dist/literal-acceptance/index.js +1 -1
- package/dist/literal-acceptance/run.d.ts +2 -0
- package/dist/literal-acceptance/run.js +19 -1
- package/dist/orphan-active/candidate-scope.d.ts +53 -0
- package/dist/orphan-active/candidate-scope.js +157 -0
- package/dist/orphan-active/evaluate.d.ts +52 -0
- package/dist/orphan-active/evaluate.js +250 -128
- package/dist/orphan-active/index.d.ts +2 -0
- package/dist/orphan-active/index.js +2 -0
- package/dist/orphan-active/issue-state.d.ts +121 -0
- package/dist/orphan-active/issue-state.js +278 -0
- package/dist/platform/cursor-managed-runtime.d.ts +95 -0
- package/dist/platform/cursor-managed-runtime.js +241 -0
- package/dist/platform/index.d.ts +1 -0
- package/dist/platform/index.js +1 -0
- package/dist/platform/platform-capabilities.d.ts +23 -1
- package/dist/platform/platform-capabilities.js +55 -15
- package/dist/policy/ceremony-dial.js +5 -9
- package/dist/policy/host-hooks.js +4 -9
- package/dist/policy/merge-approval-head.js +7 -6
- package/dist/policy/org-force-on-migration.js +5 -10
- package/dist/policy/plan-extensions.d.ts +14 -2
- package/dist/policy/plan-extensions.js +24 -3
- package/dist/policy/product-signal.js +5 -10
- package/dist/policy/require-human-merge.js +5 -9
- package/dist/policy/resolve.js +77 -16
- package/dist/policy/value-feedback.js +9 -18
- package/dist/pr-closeout-attestable/evaluate.d.ts +80 -0
- package/dist/pr-closeout-attestable/evaluate.js +306 -0
- package/dist/pr-closeout-attestable/index.d.ts +2 -0
- package/dist/pr-closeout-attestable/index.js +2 -0
- package/dist/pr-closing-keywords/gh.js +32 -9
- package/dist/pr-closing-keywords/main.d.ts +1 -0
- package/dist/pr-closing-keywords/main.js +68 -3
- package/dist/pr-closing-keywords/types.d.ts +2 -0
- package/dist/pr-merge-readiness/gh.js +32 -9
- package/dist/pr-protected-issues/gh.d.ts +6 -2
- package/dist/pr-protected-issues/gh.js +42 -11
- package/dist/pr-wait-mergeable/cascade.d.ts +9 -1
- package/dist/pr-wait-mergeable/cascade.js +21 -1
- package/dist/pr-wait-mergeable/types.d.ts +2 -0
- package/dist/pr-wait-mergeable/wrappers.d.ts +6 -0
- package/dist/pr-wait-mergeable/wrappers.js +14 -3
- package/dist/preflight/evaluate.d.ts +15 -0
- package/dist/preflight/evaluate.js +29 -3
- package/dist/product-first-done-gate/acceptance-resolver.d.ts +14 -2
- package/dist/product-first-done-gate/acceptance-resolver.js +13 -2
- package/dist/product-first-done-gate/evaluate.js +23 -9
- package/dist/product-first-done-gate/types.js +2 -0
- package/dist/release/cli-drift-report.d.ts +68 -0
- package/dist/release/cli-drift-report.js +189 -0
- package/dist/release/consumer-hard-stops.d.ts +48 -0
- package/dist/release/consumer-hard-stops.js +140 -0
- package/dist/release/consumer-readiness-disclosure.d.ts +18 -0
- package/dist/release/consumer-readiness-disclosure.js +51 -0
- package/dist/release/index.d.ts +3 -0
- package/dist/release/index.js +3 -0
- package/dist/release/issue-state-fetch.d.ts +5 -3
- package/dist/release/issue-state-fetch.js +86 -21
- package/dist/release/native-steps.js +1 -0
- package/dist/release/pipeline.js +74 -0
- package/dist/release/run-consumer-readiness.d.ts +15 -0
- package/dist/release/run-consumer-readiness.js +24 -0
- package/dist/release/types.d.ts +20 -0
- package/dist/render/constants.d.ts +4 -0
- package/dist/render/constants.js +11 -8
- package/dist/render/export-spec.js +31 -5
- package/dist/render/index.d.ts +1 -1
- package/dist/render/index.js +1 -1
- package/dist/render/prd-render.d.ts +4 -1
- package/dist/render/prd-render.js +82 -23
- package/dist/render/project-render.js +14 -7
- package/dist/render/scope-outlook.d.ts +2 -0
- package/dist/render/scope-outlook.js +3 -0
- package/dist/render/spec-render.js +2 -2
- package/dist/resolution/package-manager.d.ts +33 -4
- package/dist/resolution/package-manager.js +210 -9
- package/dist/review-monitor/constants.d.ts +7 -0
- package/dist/review-monitor/constants.js +7 -0
- package/dist/review-monitor/github-lease.d.ts +98 -1
- package/dist/review-monitor/github-lease.js +186 -4
- package/dist/review-monitor/lease-comment.d.ts +36 -0
- package/dist/review-monitor/lease-comment.js +103 -2
- package/dist/run-summary/types.d.ts +2 -2
- package/dist/scm/build-command.d.ts +2 -2
- package/dist/scm/build-command.js +2 -2
- package/dist/scm/call-shape.d.ts +25 -0
- package/dist/scm/call-shape.js +59 -0
- package/dist/scm/call.d.ts +7 -4
- package/dist/scm/call.js +48 -9
- package/dist/scm/design-critique-chip.d.ts +1 -0
- package/dist/scm/design-critique-chip.js +22 -6
- package/dist/scm/gh-rest.d.ts +34 -13
- package/dist/scm/gh-rest.js +168 -15
- package/dist/scm/index.d.ts +2 -0
- package/dist/scm/index.js +2 -0
- package/dist/scm/readiness.d.ts +2 -0
- package/dist/scm/readiness.js +28 -4
- package/dist/scm/spawn-status.d.ts +33 -0
- package/dist/scm/spawn-status.js +53 -0
- package/dist/scope/acceptance-evidence.d.ts +1 -1
- package/dist/scope/project-definition-sync.js +14 -7
- package/dist/scope/transition.js +4 -14
- package/dist/session/ac-pass-banking.d.ts +2 -2
- package/dist/session/ac-pass-banking.js +2 -2
- package/dist/session/git.d.ts +24 -0
- package/dist/session/git.js +138 -2
- package/dist/session/host-session-owner.d.ts +53 -0
- package/dist/session/host-session-owner.js +89 -0
- package/dist/session/index.d.ts +1 -0
- package/dist/session/index.js +1 -0
- package/dist/session/occupancy.d.ts +259 -13
- package/dist/session/occupancy.js +877 -33
- package/dist/session/ritual-sentinel.d.ts +5 -0
- package/dist/session/ritual-sentinel.js +5 -0
- package/dist/session/session-ready.d.ts +5 -1
- package/dist/session/session-ready.js +98 -8
- package/dist/session/session-start.d.ts +5 -1
- package/dist/session/session-start.js +32 -17
- package/dist/session/toolchain-preflight.d.ts +13 -3
- package/dist/session/toolchain-preflight.js +93 -18
- package/dist/session/verify-ac-session-cache.d.ts +2 -2
- package/dist/session/verify-ac-session-cache.js +2 -2
- package/dist/session/verify-session-ritual.d.ts +11 -0
- package/dist/session/verify-session-ritual.js +60 -15
- package/dist/slice/constants.d.ts +1 -1
- package/dist/spec-authority/constants.d.ts +10 -2
- package/dist/spec-authority/constants.js +53 -8
- package/dist/spec-authority/resolver.d.ts +3 -0
- package/dist/spec-authority/resolver.js +55 -7
- package/dist/subprocess/max-buffer.d.ts +14 -0
- package/dist/subprocess/max-buffer.js +15 -0
- package/dist/swarm/complete-cohort.d.ts +2 -0
- package/dist/swarm/complete-cohort.js +21 -13
- package/dist/swarm/index.d.ts +1 -0
- package/dist/swarm/index.js +1 -0
- package/dist/swarm/launch-cli.js +53 -33
- package/dist/swarm/launch.d.ts +8 -0
- package/dist/swarm/launch.js +63 -43
- package/dist/swarm/pre-dispatch-cli.js +2 -1
- package/dist/swarm/pre-dispatch.js +10 -1
- package/dist/swarm/subagent-status-dir.d.ts +27 -0
- package/dist/swarm/subagent-status-dir.js +42 -0
- package/dist/swarm/worktrees.d.ts +17 -3
- package/dist/swarm/worktrees.js +48 -4
- package/dist/triage/evaluate/worktrees.js +153 -6
- package/dist/triage/scope/mutations-core.d.ts +2 -6
- package/dist/triage/scope/mutations-core.js +23 -55
- package/dist/triage/scope-drift/add-ignore.js +5 -39
- package/dist/triage/subscribe/index.d.ts +2 -6
- package/dist/triage/subscribe/index.js +15 -65
- package/dist/triage/welcome/writers.js +15 -74
- package/dist/umbrella-current-shape/index.d.ts +51 -3
- package/dist/umbrella-current-shape/index.js +106 -18
- package/dist/validate-content/deposit-required.d.ts +39 -0
- package/dist/validate-content/deposit-required.js +147 -0
- package/dist/validate-content/index.d.ts +1 -0
- package/dist/validate-content/index.js +1 -0
- package/dist/validate-content/validate-links.d.ts +2 -3
- package/dist/validate-content/validate-links.js +29 -2
- package/dist/vbrief-activate/activate.d.ts +7 -2
- package/dist/vbrief-activate/activate.js +26 -13
- package/dist/vbrief-build/index.d.ts +2 -1
- package/dist/vbrief-build/index.js +2 -1
- package/dist/vbrief-build/parity-scenarios.js +5 -5
- package/dist/vbrief-build/project-definition-io.d.ts +63 -3
- package/dist/vbrief-build/project-definition-io.js +357 -31
- package/dist/vbrief-build/project-definition-mutation.d.ts +32 -0
- package/dist/vbrief-build/project-definition-mutation.js +43 -0
- package/dist/vbrief-validate/conformance.d.ts +1 -0
- package/dist/vbrief-validate/conformance.js +85 -8
- package/dist/vbrief-validate/main.js +2 -0
- package/dist/vbrief-validate/precutover.js +5 -10
- package/dist/verify-ac/clauses.d.ts +62 -2
- package/dist/verify-ac/clauses.js +183 -111
- package/dist/verify-ac/evaluate.d.ts +9 -0
- package/dist/verify-ac/evaluate.js +32 -9
- package/dist/verify-ac/index.d.ts +1 -1
- package/dist/verify-ac/index.js +1 -1
- package/dist/verify-env/node-runtime.d.ts +8 -4
- package/dist/verify-env/node-runtime.js +9 -6
- package/dist/verify-env/toolchain-check.d.ts +35 -3
- package/dist/verify-env/toolchain-check.js +155 -36
- package/dist/verify-source/deposit-closure.d.ts +23 -0
- package/dist/verify-source/deposit-closure.js +162 -0
- package/dist/verify-source/index.d.ts +2 -0
- package/dist/verify-source/index.js +2 -0
- package/dist/verify-source/project-definition-mutation-boundary.d.ts +104 -0
- package/dist/verify-source/project-definition-mutation-boundary.js +326 -0
- package/dist/verify-source/semantic-single-source.d.ts +36 -0
- package/dist/verify-source/semantic-single-source.js +349 -0
- package/package.json +7 -3
|
@@ -0,0 +1,278 @@
|
|
|
1
|
+
import { existsSync, readFileSync, statSync } from "node:fs";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import { REPO_RE } from "../cache/constants.js";
|
|
4
|
+
import { defaultRunGh } from "../pr-protected-issues/gh.js";
|
|
5
|
+
import { defaultWhich, resolveBinary } from "../scm/binary.js";
|
|
6
|
+
import { ScmStubError } from "../scm/errors.js";
|
|
7
|
+
import { restIssueListOpenInventory } from "../scm/gh-rest.js";
|
|
8
|
+
import { CACHE_DIR_NAME, CACHE_SOURCE_GITHUB_ISSUE } from "../triage/queue/constants.js";
|
|
9
|
+
/**
|
|
10
|
+
* A cache hit older than this is no longer evidence (#3767). The measured
|
|
11
|
+
* defect was a ~12 h old cached `open` suppressing the live read that would
|
|
12
|
+
* have corrected it; 15 min keeps warm-cache offline runs working while making
|
|
13
|
+
* an overnight entry non-authoritative.
|
|
14
|
+
*/
|
|
15
|
+
export const ISSUE_CACHE_MAX_AGE_MS = 15 * 60 * 1000;
|
|
16
|
+
/**
|
|
17
|
+
* Latency budget for scoped `--issue N`: one authoritative issue read plus at
|
|
18
|
+
* most one linked-PR read. Measured ~0.75 s per `gh api` read; the headroom
|
|
19
|
+
* covers a slow network or a `ghx` proxy hop.
|
|
20
|
+
*/
|
|
21
|
+
export const SCOPED_LATENCY_BUDGET_MS = 5_000;
|
|
22
|
+
/**
|
|
23
|
+
* Latency budget for the unscoped aggregate sweep. One complete open-issue
|
|
24
|
+
* inventory measured 4.2 s constant on deftai/directive (560 open issues),
|
|
25
|
+
* against 14.2 s and 76.6 s for per-brief reads at the WIP cap of 20 on two
|
|
26
|
+
* hosts. The budget is sized for the inventory plus a few confirming reads and
|
|
27
|
+
* keeps the gate inside the fast-preflight tier.
|
|
28
|
+
*/
|
|
29
|
+
export const AGGREGATE_LATENCY_BUDGET_MS = 15_000;
|
|
30
|
+
/** `resolveBinary` view that hides `ghx`, so this gate reads through plain `gh` when it exists. */
|
|
31
|
+
const GH_ONLY_WHICH = (name) => (name === "ghx" ? null : defaultWhich(name));
|
|
32
|
+
/**
|
|
33
|
+
* Pin plain `gh` for this gate's authoritative reads (#3767). `resolveBinary`
|
|
34
|
+
* prefers `ghx` — a cached GET proxy — so an unpinned "live" read is itself a
|
|
35
|
+
* cache. When `gh` is absent we still run, and report the proxy caveat rather
|
|
36
|
+
* than claiming freshness we cannot deliver. Whether `ghx` should be preferred
|
|
37
|
+
* at all is #3737.
|
|
38
|
+
*/
|
|
39
|
+
export function makeGateRunner() {
|
|
40
|
+
let pinned = null;
|
|
41
|
+
try {
|
|
42
|
+
pinned = resolveBinary(GH_ONLY_WHICH);
|
|
43
|
+
}
|
|
44
|
+
catch {
|
|
45
|
+
pinned = null;
|
|
46
|
+
}
|
|
47
|
+
if (pinned === null) {
|
|
48
|
+
return { runGh: defaultRunGh, proxied: true };
|
|
49
|
+
}
|
|
50
|
+
const binary = pinned;
|
|
51
|
+
return { runGh: (cmd) => defaultRunGh(cmd, binary), proxied: false };
|
|
52
|
+
}
|
|
53
|
+
function isSafeRepo(repo) {
|
|
54
|
+
return REPO_RE.test(repo);
|
|
55
|
+
}
|
|
56
|
+
function readJson(path) {
|
|
57
|
+
try {
|
|
58
|
+
const parsed = JSON.parse(readFileSync(path, "utf8"));
|
|
59
|
+
return typeof parsed === "object" && parsed !== null && !Array.isArray(parsed)
|
|
60
|
+
? parsed
|
|
61
|
+
: null;
|
|
62
|
+
}
|
|
63
|
+
catch {
|
|
64
|
+
return null;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
function cacheEntryDir(projectRoot, ref) {
|
|
68
|
+
const [owner, name] = ref.repo.split("/", 2);
|
|
69
|
+
if (!owner || !name) {
|
|
70
|
+
return null;
|
|
71
|
+
}
|
|
72
|
+
return join(projectRoot, CACHE_DIR_NAME, CACHE_SOURCE_GITHUB_ISSUE, owner, name, String(ref.number));
|
|
73
|
+
}
|
|
74
|
+
function cacheEntryAgeMs(entryDir, rawPath, nowMs) {
|
|
75
|
+
const meta = readJson(join(entryDir, "meta.json"));
|
|
76
|
+
const stamp = meta?.fetched_at ?? meta?.cached_at ?? meta?.updated_at;
|
|
77
|
+
if (typeof stamp === "string") {
|
|
78
|
+
const parsed = Date.parse(stamp);
|
|
79
|
+
if (Number.isFinite(parsed)) {
|
|
80
|
+
return Math.max(0, nowMs - parsed);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
try {
|
|
84
|
+
return Math.max(0, nowMs - statSync(rawPath).mtimeMs);
|
|
85
|
+
}
|
|
86
|
+
catch {
|
|
87
|
+
return null;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
const CACHE_MISS = { state: null, ageMs: null };
|
|
91
|
+
/** Read the triage-cache issue state together with the entry's age (#3767). */
|
|
92
|
+
export function readCachedIssue(projectRoot, ref, nowMs) {
|
|
93
|
+
const entryDir = cacheEntryDir(projectRoot, ref);
|
|
94
|
+
if (entryDir === null) {
|
|
95
|
+
return CACHE_MISS;
|
|
96
|
+
}
|
|
97
|
+
const rawPath = join(entryDir, "raw.json");
|
|
98
|
+
if (!existsSync(rawPath)) {
|
|
99
|
+
return CACHE_MISS;
|
|
100
|
+
}
|
|
101
|
+
const raw = readJson(rawPath);
|
|
102
|
+
if (raw === null) {
|
|
103
|
+
return CACHE_MISS;
|
|
104
|
+
}
|
|
105
|
+
const state = typeof raw.state === "string" ? raw.state.toLowerCase() : "";
|
|
106
|
+
if (state !== "open" && state !== "closed") {
|
|
107
|
+
return CACHE_MISS;
|
|
108
|
+
}
|
|
109
|
+
return { state, ageMs: cacheEntryAgeMs(entryDir, rawPath, nowMs) };
|
|
110
|
+
}
|
|
111
|
+
/** Authoritative single-issue REST read. Returns `null` on any failure. */
|
|
112
|
+
export function fetchIssueStateLive(ref, runGh) {
|
|
113
|
+
const result = runGh(["gh", "api", `repos/${ref.repo}/issues/${ref.number}`]);
|
|
114
|
+
if (result.returncode !== 0) {
|
|
115
|
+
return null;
|
|
116
|
+
}
|
|
117
|
+
try {
|
|
118
|
+
const payload = JSON.parse(result.stdout);
|
|
119
|
+
if (payload === null || typeof payload !== "object") {
|
|
120
|
+
return null;
|
|
121
|
+
}
|
|
122
|
+
const state = payload.state;
|
|
123
|
+
return state === "open" || state === "closed" ? state : null;
|
|
124
|
+
}
|
|
125
|
+
catch {
|
|
126
|
+
return null;
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
/**
|
|
130
|
+
* One complete open-issue inventory per repo per run (#3767, reusing #3752).
|
|
131
|
+
* `restIssueListOpenInventory` fails closed on command failure, non-array JSON,
|
|
132
|
+
* malformed rows, buffer exhaustion, and pagination cap — so absence from a
|
|
133
|
+
* successful inventory really does mean "not open", and a truncated read is an
|
|
134
|
+
* error rather than a silent "closed".
|
|
135
|
+
*/
|
|
136
|
+
export class OpenIssueInventory {
|
|
137
|
+
runGh;
|
|
138
|
+
byRepo = new Map();
|
|
139
|
+
confirmations = new Map();
|
|
140
|
+
constructor(runGh) {
|
|
141
|
+
this.runGh = runGh;
|
|
142
|
+
}
|
|
143
|
+
lookup(repo) {
|
|
144
|
+
const memo = this.byRepo.get(repo);
|
|
145
|
+
if (memo !== undefined) {
|
|
146
|
+
return memo;
|
|
147
|
+
}
|
|
148
|
+
const resolved = this.fetch(repo);
|
|
149
|
+
this.byRepo.set(repo, resolved);
|
|
150
|
+
return resolved;
|
|
151
|
+
}
|
|
152
|
+
/**
|
|
153
|
+
* Confirm an inventory absence with one authoritative read, reused for the
|
|
154
|
+
* rest of this run when several briefs name the same issue (#3767).
|
|
155
|
+
*/
|
|
156
|
+
confirm(ref) {
|
|
157
|
+
const key = `${ref.repo}#${ref.number}`;
|
|
158
|
+
if (this.confirmations.has(key)) {
|
|
159
|
+
return this.confirmations.get(key) ?? null;
|
|
160
|
+
}
|
|
161
|
+
const confirmed = fetchIssueStateLive(ref, this.runGh);
|
|
162
|
+
this.confirmations.set(key, confirmed);
|
|
163
|
+
return confirmed;
|
|
164
|
+
}
|
|
165
|
+
fetch(repo) {
|
|
166
|
+
try {
|
|
167
|
+
const rows = restIssueListOpenInventory(repo, {
|
|
168
|
+
runGhApiFn: (args) => this.runGh(["gh", "api", ...args]),
|
|
169
|
+
});
|
|
170
|
+
const numbers = new Set();
|
|
171
|
+
for (const row of rows) {
|
|
172
|
+
const n = row.number;
|
|
173
|
+
if (typeof n === "number" && Number.isInteger(n)) {
|
|
174
|
+
numbers.add(n);
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
return { numbers };
|
|
178
|
+
}
|
|
179
|
+
catch (err) {
|
|
180
|
+
if (err instanceof ScmStubError) {
|
|
181
|
+
throw err;
|
|
182
|
+
}
|
|
183
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
184
|
+
return { error: message.split("\n")[0] ?? "unknown error" };
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
function unverified(detail) {
|
|
189
|
+
return { state: null, basis: "unverified", detail };
|
|
190
|
+
}
|
|
191
|
+
function freshOrNull(cached) {
|
|
192
|
+
return cached.state !== null && cached.ageMs !== null && cached.ageMs <= ISSUE_CACHE_MAX_AGE_MS
|
|
193
|
+
? { state: cached.state, ageMs: cached.ageMs }
|
|
194
|
+
: null;
|
|
195
|
+
}
|
|
196
|
+
function fromCache(fresh) {
|
|
197
|
+
return { state: fresh.state, basis: "cache", cacheAgeMs: fresh.ageMs };
|
|
198
|
+
}
|
|
199
|
+
function staleDetail(cached) {
|
|
200
|
+
const age = cached.ageMs === null ? "unknown age" : formatAge(cached.ageMs);
|
|
201
|
+
return `cached ${cached.state} is ${age} old (max ${formatAge(ISSUE_CACHE_MAX_AGE_MS)})`;
|
|
202
|
+
}
|
|
203
|
+
/** Human-readable duration for basis reporting: `42s`, `9m`, `12h`, `3d`. */
|
|
204
|
+
export function formatAge(ms) {
|
|
205
|
+
const seconds = Math.round(ms / 1000);
|
|
206
|
+
if (seconds < 60) {
|
|
207
|
+
return `${seconds}s`;
|
|
208
|
+
}
|
|
209
|
+
const minutes = Math.round(seconds / 60);
|
|
210
|
+
if (minutes < 60) {
|
|
211
|
+
return `${minutes}m`;
|
|
212
|
+
}
|
|
213
|
+
const hours = Math.round(minutes / 60);
|
|
214
|
+
return hours < 48 ? `${hours}h` : `${Math.round(hours / 24)}d`;
|
|
215
|
+
}
|
|
216
|
+
/**
|
|
217
|
+
* Scoped `--issue N`: a proof obligation before DONE, so read authoritatively
|
|
218
|
+
* first and accept a cache hit only inside the freshness bound. Unknown stays
|
|
219
|
+
* fail-closed at the call site.
|
|
220
|
+
*/
|
|
221
|
+
export function resolveIssueStateScoped(ref, ctx) {
|
|
222
|
+
if (!isSafeRepo(ref.repo)) {
|
|
223
|
+
return unverified(`reference repo '${ref.repo}' is not a valid owner/repo slug`);
|
|
224
|
+
}
|
|
225
|
+
if (!ctx.skipGh) {
|
|
226
|
+
const live = fetchIssueStateLive(ref, ctx.runGh);
|
|
227
|
+
if (live !== null) {
|
|
228
|
+
return { state: live, basis: "live" };
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
const cached = readCachedIssue(ctx.projectRoot, ref, ctx.nowMs);
|
|
232
|
+
const fresh = freshOrNull(cached);
|
|
233
|
+
if (fresh !== null) {
|
|
234
|
+
return fromCache(fresh);
|
|
235
|
+
}
|
|
236
|
+
if (cached.state !== null) {
|
|
237
|
+
return unverified(`${staleDetail(cached)} and the authoritative read was unavailable`);
|
|
238
|
+
}
|
|
239
|
+
return unverified(ctx.skipGh ? "--skip-gh with no cache entry" : "live issue read failed and no cache entry");
|
|
240
|
+
}
|
|
241
|
+
/**
|
|
242
|
+
* Unscoped aggregate: resolve every reference from one complete open-issue
|
|
243
|
+
* inventory rather than a live read per brief. Membership means open. Absence
|
|
244
|
+
* means "not open", which is the direction that tells an operator to run
|
|
245
|
+
* `scope:complete`, so it is confirmed by one authoritative read before the
|
|
246
|
+
* gate acts on it.
|
|
247
|
+
*/
|
|
248
|
+
export function resolveIssueStateAggregate(ref, ctx) {
|
|
249
|
+
if (!isSafeRepo(ref.repo)) {
|
|
250
|
+
return unverified(`reference repo '${ref.repo}' is not a valid owner/repo slug`);
|
|
251
|
+
}
|
|
252
|
+
const cached = readCachedIssue(ctx.projectRoot, ref, ctx.nowMs);
|
|
253
|
+
const fresh = freshOrNull(cached);
|
|
254
|
+
if (ctx.skipGh) {
|
|
255
|
+
if (fresh !== null) {
|
|
256
|
+
return fromCache(fresh);
|
|
257
|
+
}
|
|
258
|
+
return unverified(cached.state === null
|
|
259
|
+
? "--skip-gh with no cache entry"
|
|
260
|
+
: `--skip-gh and ${staleDetail(cached)}`);
|
|
261
|
+
}
|
|
262
|
+
const inventory = ctx.inventory.lookup(ref.repo);
|
|
263
|
+
if ("error" in inventory) {
|
|
264
|
+
if (fresh !== null) {
|
|
265
|
+
return fromCache(fresh);
|
|
266
|
+
}
|
|
267
|
+
return unverified(`open-issue inventory unavailable: ${inventory.error}`);
|
|
268
|
+
}
|
|
269
|
+
if (inventory.numbers.has(ref.number)) {
|
|
270
|
+
return { state: "open", basis: "inventory" };
|
|
271
|
+
}
|
|
272
|
+
const confirmed = ctx.inventory.confirm(ref);
|
|
273
|
+
if (confirmed !== null) {
|
|
274
|
+
return { state: confirmed, basis: "live" };
|
|
275
|
+
}
|
|
276
|
+
return unverified("absent from the open-issue inventory but the confirming read failed");
|
|
277
|
+
}
|
|
278
|
+
//# sourceMappingURL=issue-state.js.map
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Cursor managed-runtime probe and explicit host-gh selection (#3859).
|
|
3
|
+
*
|
|
4
|
+
* `CURSOR_AGENT` is set by local desktop Cursor, by Cursor-managed cloud VMs,
|
|
5
|
+
* and by Windows "My Machines" workers alike, so it cannot decide the runtime on
|
|
6
|
+
* its own. Two routes were considered and rejected:
|
|
7
|
+
*
|
|
8
|
+
* - Dropping the `CURSOR_AGENT` hop grants host credentials on a managed VM
|
|
9
|
+
* by marker *absence*.
|
|
10
|
+
* - `process.platform === "win32"` is not an invariant. Cursor runs Windows
|
|
11
|
+
* My Machines workers under a cloud agent loop, and "managed VMs are Ubuntu"
|
|
12
|
+
* is a versioned fact about a third party's fleet, not a runtime property.
|
|
13
|
+
*
|
|
14
|
+
* What remains is a genuine asymmetry:
|
|
15
|
+
*
|
|
16
|
+
* - Cursor-managed VMs serve a local metadata API on `CURSOR_AGENT_SOCKET`
|
|
17
|
+
* whose `agent/runtime` is always `managed`. A successful read is a
|
|
18
|
+
* *positive* assertion of managed cloud, so it is a sound deny signal.
|
|
19
|
+
* - There is no positive local-desktop signal. Absence of the socket means
|
|
20
|
+
* "not managed, or unreachable" and MUST NOT select host credentials --
|
|
21
|
+
* that is the marker-absence grant this module exists to prevent. Absence
|
|
22
|
+
* leaves the runtime ambiguous, and an ambiguous runtime requires an
|
|
23
|
+
* explicit operator or dispatcher selection.
|
|
24
|
+
*
|
|
25
|
+
* Both probe error directions are safe. A false `managed` denies host
|
|
26
|
+
* credentials (fail closed). A failed read leaves the runtime ambiguous, which
|
|
27
|
+
* preserves today's behaviour unless an operator opted in.
|
|
28
|
+
*/
|
|
29
|
+
/** Env var carrying the Cursor managed-runtime metadata socket path. */
|
|
30
|
+
export declare const MANAGED_RUNTIME_SOCKET_ENV = "CURSOR_AGENT_SOCKET";
|
|
31
|
+
/** Metadata API route reporting the runtime kind. */
|
|
32
|
+
export declare const MANAGED_RUNTIME_PATH = "/agent/runtime";
|
|
33
|
+
/** The only `agent/runtime` value Cursor-managed VMs report. */
|
|
34
|
+
export declare const MANAGED_RUNTIME_VALUE = "managed";
|
|
35
|
+
/**
|
|
36
|
+
* Env var an operator or dispatcher sets to resolve an ambiguous Cursor
|
|
37
|
+
* runtime. Named for the `github_auth_mode` dispatch-envelope field so a
|
|
38
|
+
* dispatcher exports the same label it already records.
|
|
39
|
+
*/
|
|
40
|
+
export declare const GITHUB_AUTH_MODE_ENV = "DEFT_GITHUB_AUTH_MODE";
|
|
41
|
+
/** Stable reason ids naming why a runtime mode was chosen (#3859). */
|
|
42
|
+
export declare const RUNTIME_REASON_CI_MARKER = "ci-marker";
|
|
43
|
+
export declare const RUNTIME_REASON_MANAGED_RUNTIME_PROBE = "cursor-managed-runtime-probe";
|
|
44
|
+
export declare const RUNTIME_REASON_CURSOR_SANDBOX_MARKER = "cursor-sandbox-marker";
|
|
45
|
+
export declare const RUNTIME_REASON_CURSOR_MARKER_AMBIGUOUS = "cursor-marker-runtime-ambiguous";
|
|
46
|
+
export declare const RUNTIME_REASON_EXPLICIT_HOST_GH = "explicit-host-gh-selection";
|
|
47
|
+
export declare const RUNTIME_REASON_NO_RUNTIME_MARKER = "no-runtime-marker";
|
|
48
|
+
export type ManagedRuntimeVerdict = "managed" | "not-managed" | "unavailable";
|
|
49
|
+
export interface ManagedRuntimeProbeResult {
|
|
50
|
+
/** `managed` is the only verdict that may deny host credentials. */
|
|
51
|
+
readonly verdict: ManagedRuntimeVerdict;
|
|
52
|
+
readonly socketPath: string | null;
|
|
53
|
+
/** Short non-secret diagnostic for operator-visible output. */
|
|
54
|
+
readonly detail: string;
|
|
55
|
+
}
|
|
56
|
+
export type ManagedRuntimeProbe = (environ: Readonly<Record<string, string | undefined>>) => ManagedRuntimeProbeResult;
|
|
57
|
+
/**
|
|
58
|
+
* True when a metadata body reports the managed runtime.
|
|
59
|
+
*
|
|
60
|
+
* Accepts a JSON object carrying a `runtime` (or `agentRuntime` / `agent_runtime`)
|
|
61
|
+
* field and a bare string body, because the exact response shape is documented
|
|
62
|
+
* only as "`agent/runtime` is always `managed`". Over-matching here denies host
|
|
63
|
+
* credentials, which is the safe direction.
|
|
64
|
+
*/
|
|
65
|
+
export declare function bodyReportsManagedRuntime(body: string): boolean;
|
|
66
|
+
/** Shape the reader child writes to stdout. */
|
|
67
|
+
interface ProbeResponse {
|
|
68
|
+
readonly status?: number;
|
|
69
|
+
readonly body?: string;
|
|
70
|
+
readonly error?: string;
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* Interpret the reader child's stdout, or null when it is not a usable object.
|
|
74
|
+
*
|
|
75
|
+
* Exported for the same reason as `bodyReportsManagedRuntime`: it is the parsing
|
|
76
|
+
* step and it has to be provably total. `JSON.parse("null")` returns null
|
|
77
|
+
* without throwing, so a bare try/catch around the parse would still leave the
|
|
78
|
+
* field reads that follow able to throw -- and a probe that throws aborts
|
|
79
|
+
* classification instead of failing closed.
|
|
80
|
+
*/
|
|
81
|
+
export declare function parseProbeResponse(stdout: string): ProbeResponse | null;
|
|
82
|
+
/** Clear the probe memo. Test seam only. */
|
|
83
|
+
export declare function resetManagedRuntimeProbeCache(): void;
|
|
84
|
+
/** Default managed-runtime probe. Never throws. */
|
|
85
|
+
export declare const probeManagedRuntime: ManagedRuntimeProbe;
|
|
86
|
+
/**
|
|
87
|
+
* True when an operator or dispatcher explicitly selected host-gh for this
|
|
88
|
+
* execution environment. This resolves an *ambiguous* runtime only; it can
|
|
89
|
+
* never override a positive managed-runtime read.
|
|
90
|
+
*/
|
|
91
|
+
export declare function hasExplicitHostGhSelection(environ: Readonly<Record<string, string | undefined>>): boolean;
|
|
92
|
+
/** Remediation naming the opt-in, for operator-visible not-ready output. */
|
|
93
|
+
export declare const EXPLICIT_SELECTION_REMEDIATION: string;
|
|
94
|
+
export {};
|
|
95
|
+
//# sourceMappingURL=cursor-managed-runtime.d.ts.map
|
|
@@ -0,0 +1,241 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Cursor managed-runtime probe and explicit host-gh selection (#3859).
|
|
3
|
+
*
|
|
4
|
+
* `CURSOR_AGENT` is set by local desktop Cursor, by Cursor-managed cloud VMs,
|
|
5
|
+
* and by Windows "My Machines" workers alike, so it cannot decide the runtime on
|
|
6
|
+
* its own. Two routes were considered and rejected:
|
|
7
|
+
*
|
|
8
|
+
* - Dropping the `CURSOR_AGENT` hop grants host credentials on a managed VM
|
|
9
|
+
* by marker *absence*.
|
|
10
|
+
* - `process.platform === "win32"` is not an invariant. Cursor runs Windows
|
|
11
|
+
* My Machines workers under a cloud agent loop, and "managed VMs are Ubuntu"
|
|
12
|
+
* is a versioned fact about a third party's fleet, not a runtime property.
|
|
13
|
+
*
|
|
14
|
+
* What remains is a genuine asymmetry:
|
|
15
|
+
*
|
|
16
|
+
* - Cursor-managed VMs serve a local metadata API on `CURSOR_AGENT_SOCKET`
|
|
17
|
+
* whose `agent/runtime` is always `managed`. A successful read is a
|
|
18
|
+
* *positive* assertion of managed cloud, so it is a sound deny signal.
|
|
19
|
+
* - There is no positive local-desktop signal. Absence of the socket means
|
|
20
|
+
* "not managed, or unreachable" and MUST NOT select host credentials --
|
|
21
|
+
* that is the marker-absence grant this module exists to prevent. Absence
|
|
22
|
+
* leaves the runtime ambiguous, and an ambiguous runtime requires an
|
|
23
|
+
* explicit operator or dispatcher selection.
|
|
24
|
+
*
|
|
25
|
+
* Both probe error directions are safe. A false `managed` denies host
|
|
26
|
+
* credentials (fail closed). A failed read leaves the runtime ambiguous, which
|
|
27
|
+
* preserves today's behaviour unless an operator opted in.
|
|
28
|
+
*/
|
|
29
|
+
import { spawnSync } from "node:child_process";
|
|
30
|
+
/**
|
|
31
|
+
* The `host-gh` auth-mode label. Duplicated from intake/github-auth-modes.ts
|
|
32
|
+
* rather than imported: that module imports the runtime classifier, which
|
|
33
|
+
* imports this one.
|
|
34
|
+
*/
|
|
35
|
+
const HOST_GH_SELECTION = "host-gh";
|
|
36
|
+
/** Env var carrying the Cursor managed-runtime metadata socket path. */
|
|
37
|
+
export const MANAGED_RUNTIME_SOCKET_ENV = "CURSOR_AGENT_SOCKET";
|
|
38
|
+
/** Metadata API route reporting the runtime kind. */
|
|
39
|
+
export const MANAGED_RUNTIME_PATH = "/agent/runtime";
|
|
40
|
+
/** The only `agent/runtime` value Cursor-managed VMs report. */
|
|
41
|
+
export const MANAGED_RUNTIME_VALUE = "managed";
|
|
42
|
+
/**
|
|
43
|
+
* Env var an operator or dispatcher sets to resolve an ambiguous Cursor
|
|
44
|
+
* runtime. Named for the `github_auth_mode` dispatch-envelope field so a
|
|
45
|
+
* dispatcher exports the same label it already records.
|
|
46
|
+
*/
|
|
47
|
+
export const GITHUB_AUTH_MODE_ENV = "DEFT_GITHUB_AUTH_MODE";
|
|
48
|
+
/** Stable reason ids naming why a runtime mode was chosen (#3859). */
|
|
49
|
+
export const RUNTIME_REASON_CI_MARKER = "ci-marker";
|
|
50
|
+
export const RUNTIME_REASON_MANAGED_RUNTIME_PROBE = "cursor-managed-runtime-probe";
|
|
51
|
+
export const RUNTIME_REASON_CURSOR_SANDBOX_MARKER = "cursor-sandbox-marker";
|
|
52
|
+
export const RUNTIME_REASON_CURSOR_MARKER_AMBIGUOUS = "cursor-marker-runtime-ambiguous";
|
|
53
|
+
export const RUNTIME_REASON_EXPLICIT_HOST_GH = "explicit-host-gh-selection";
|
|
54
|
+
export const RUNTIME_REASON_NO_RUNTIME_MARKER = "no-runtime-marker";
|
|
55
|
+
/**
|
|
56
|
+
* Read the metadata API in a child process.
|
|
57
|
+
*
|
|
58
|
+
* The readiness and classification paths are synchronous throughout, and Node
|
|
59
|
+
* cannot read an HTTP-over-unix-socket response synchronously in-process. This
|
|
60
|
+
* runs only when `CURSOR_AGENT_SOCKET` is set, so the common local case pays
|
|
61
|
+
* nothing.
|
|
62
|
+
*/
|
|
63
|
+
const READER_SCRIPT = `
|
|
64
|
+
const http = require("node:http");
|
|
65
|
+
const done = (payload) => { process.stdout.write(JSON.stringify(payload)); };
|
|
66
|
+
const req = http.request(
|
|
67
|
+
{
|
|
68
|
+
socketPath: process.env.DEFT_PROBE_SOCKET,
|
|
69
|
+
path: process.env.DEFT_PROBE_PATH,
|
|
70
|
+
method: "GET",
|
|
71
|
+
timeout: 2000,
|
|
72
|
+
},
|
|
73
|
+
(res) => {
|
|
74
|
+
let body = "";
|
|
75
|
+
res.setEncoding("utf8");
|
|
76
|
+
res.on("data", (chunk) => { body += chunk; if (body.length > 4096) req.destroy(); });
|
|
77
|
+
res.on("end", () => done({ status: res.statusCode, body }));
|
|
78
|
+
},
|
|
79
|
+
);
|
|
80
|
+
req.on("timeout", () => { req.destroy(new Error("timeout")); });
|
|
81
|
+
req.on("error", (err) => done({ error: String((err && err.message) || err) }));
|
|
82
|
+
req.end();
|
|
83
|
+
`;
|
|
84
|
+
function unavailable(socketPath, detail) {
|
|
85
|
+
return { verdict: "unavailable", socketPath, detail };
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* True when a metadata body reports the managed runtime.
|
|
89
|
+
*
|
|
90
|
+
* Accepts a JSON object carrying a `runtime` (or `agentRuntime` / `agent_runtime`)
|
|
91
|
+
* field and a bare string body, because the exact response shape is documented
|
|
92
|
+
* only as "`agent/runtime` is always `managed`". Over-matching here denies host
|
|
93
|
+
* credentials, which is the safe direction.
|
|
94
|
+
*/
|
|
95
|
+
export function bodyReportsManagedRuntime(body) {
|
|
96
|
+
const text = body.trim();
|
|
97
|
+
if (text.length === 0)
|
|
98
|
+
return false;
|
|
99
|
+
if (text.toLowerCase() === MANAGED_RUNTIME_VALUE)
|
|
100
|
+
return true;
|
|
101
|
+
let parsed;
|
|
102
|
+
try {
|
|
103
|
+
parsed = JSON.parse(text);
|
|
104
|
+
}
|
|
105
|
+
catch {
|
|
106
|
+
return false;
|
|
107
|
+
}
|
|
108
|
+
if (typeof parsed === "string")
|
|
109
|
+
return parsed.trim().toLowerCase() === MANAGED_RUNTIME_VALUE;
|
|
110
|
+
if (typeof parsed !== "object" || parsed === null)
|
|
111
|
+
return false;
|
|
112
|
+
const record = parsed;
|
|
113
|
+
for (const key of ["runtime", "agentRuntime", "agent_runtime"]) {
|
|
114
|
+
const value = record[key];
|
|
115
|
+
if (typeof value === "string" && value.trim().toLowerCase() === MANAGED_RUNTIME_VALUE) {
|
|
116
|
+
return true;
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
return false;
|
|
120
|
+
}
|
|
121
|
+
/**
|
|
122
|
+
* Interpret the reader child's stdout, or null when it is not a usable object.
|
|
123
|
+
*
|
|
124
|
+
* Exported for the same reason as `bodyReportsManagedRuntime`: it is the parsing
|
|
125
|
+
* step and it has to be provably total. `JSON.parse("null")` returns null
|
|
126
|
+
* without throwing, so a bare try/catch around the parse would still leave the
|
|
127
|
+
* field reads that follow able to throw -- and a probe that throws aborts
|
|
128
|
+
* classification instead of failing closed.
|
|
129
|
+
*/
|
|
130
|
+
export function parseProbeResponse(stdout) {
|
|
131
|
+
let parsed;
|
|
132
|
+
try {
|
|
133
|
+
parsed = JSON.parse(stdout);
|
|
134
|
+
}
|
|
135
|
+
catch {
|
|
136
|
+
return null;
|
|
137
|
+
}
|
|
138
|
+
if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed))
|
|
139
|
+
return null;
|
|
140
|
+
return parsed;
|
|
141
|
+
}
|
|
142
|
+
/**
|
|
143
|
+
* Per-process memo of *positive* managed reads, keyed by socket path.
|
|
144
|
+
*
|
|
145
|
+
* Only the `managed` verdict is memoized, and that asymmetry is the safety
|
|
146
|
+
* property. A managed VM cannot stop being managed mid-process, so holding that
|
|
147
|
+
* deny costs nothing and keeps classification to one child spawn per socket.
|
|
148
|
+
*
|
|
149
|
+
* Every other verdict says only that the API did not assert `managed` on that
|
|
150
|
+
* attempt: the socket may not have been listening yet, or the read may have
|
|
151
|
+
* failed outright. Memoizing one would let a transient miss suppress a later
|
|
152
|
+
* deny -- a managed VM whose first probe lost the startup race would stay
|
|
153
|
+
* ambiguous for the life of the process, and an explicit `host-gh` selection
|
|
154
|
+
* would then hand it host credentials. Re-probing an ambiguous runtime costs a
|
|
155
|
+
* child spawn bounded by the timeouts below; caching one is unbounded in the
|
|
156
|
+
* only direction that matters.
|
|
157
|
+
*/
|
|
158
|
+
const managedVerdictMemo = new Map();
|
|
159
|
+
/** Clear the probe memo. Test seam only. */
|
|
160
|
+
export function resetManagedRuntimeProbeCache() {
|
|
161
|
+
managedVerdictMemo.clear();
|
|
162
|
+
}
|
|
163
|
+
/** Default managed-runtime probe. Never throws. */
|
|
164
|
+
export const probeManagedRuntime = (environ) => {
|
|
165
|
+
const socketPath = (environ[MANAGED_RUNTIME_SOCKET_ENV] ?? "").trim();
|
|
166
|
+
if (socketPath.length === 0) {
|
|
167
|
+
return unavailable(null, `${MANAGED_RUNTIME_SOCKET_ENV} is not set; runtime not asserted`);
|
|
168
|
+
}
|
|
169
|
+
const memoized = managedVerdictMemo.get(socketPath);
|
|
170
|
+
if (memoized !== undefined)
|
|
171
|
+
return memoized;
|
|
172
|
+
const result = readManagedRuntime(socketPath);
|
|
173
|
+
if (result.verdict === "managed")
|
|
174
|
+
managedVerdictMemo.set(socketPath, result);
|
|
175
|
+
return result;
|
|
176
|
+
};
|
|
177
|
+
function readManagedRuntime(socketPath) {
|
|
178
|
+
let result;
|
|
179
|
+
try {
|
|
180
|
+
result = spawnSync(process.execPath, ["-e", READER_SCRIPT], {
|
|
181
|
+
env: {
|
|
182
|
+
...process.env,
|
|
183
|
+
DEFT_PROBE_SOCKET: socketPath,
|
|
184
|
+
DEFT_PROBE_PATH: MANAGED_RUNTIME_PATH,
|
|
185
|
+
},
|
|
186
|
+
encoding: "utf8",
|
|
187
|
+
timeout: 3000,
|
|
188
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
189
|
+
windowsHide: true,
|
|
190
|
+
});
|
|
191
|
+
}
|
|
192
|
+
catch (err) {
|
|
193
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
194
|
+
return unavailable(socketPath, `metadata probe failed to start: ${message}`);
|
|
195
|
+
}
|
|
196
|
+
const stdout = typeof result.stdout === "string" ? result.stdout : "";
|
|
197
|
+
if (stdout.trim().length === 0) {
|
|
198
|
+
return unavailable(socketPath, `${MANAGED_RUNTIME_PATH} returned no response`);
|
|
199
|
+
}
|
|
200
|
+
const payload = parseProbeResponse(stdout);
|
|
201
|
+
if (payload === null) {
|
|
202
|
+
return unavailable(socketPath, `${MANAGED_RUNTIME_PATH} response was unparseable`);
|
|
203
|
+
}
|
|
204
|
+
if (typeof payload.error === "string") {
|
|
205
|
+
return unavailable(socketPath, `${MANAGED_RUNTIME_PATH} unreachable: ${payload.error}`);
|
|
206
|
+
}
|
|
207
|
+
if (payload.status !== 200) {
|
|
208
|
+
return unavailable(socketPath, `${MANAGED_RUNTIME_PATH} returned status ${payload.status}`);
|
|
209
|
+
}
|
|
210
|
+
if (bodyReportsManagedRuntime(payload.body ?? "")) {
|
|
211
|
+
return {
|
|
212
|
+
verdict: "managed",
|
|
213
|
+
socketPath,
|
|
214
|
+
detail: `${MANAGED_RUNTIME_PATH} reported ${MANAGED_RUNTIME_VALUE}`,
|
|
215
|
+
};
|
|
216
|
+
}
|
|
217
|
+
return {
|
|
218
|
+
verdict: "not-managed",
|
|
219
|
+
socketPath,
|
|
220
|
+
detail: `${MANAGED_RUNTIME_PATH} did not report ${MANAGED_RUNTIME_VALUE}`,
|
|
221
|
+
};
|
|
222
|
+
}
|
|
223
|
+
/**
|
|
224
|
+
* True when an operator or dispatcher explicitly selected host-gh for this
|
|
225
|
+
* execution environment. This resolves an *ambiguous* runtime only; it can
|
|
226
|
+
* never override a positive managed-runtime read.
|
|
227
|
+
*/
|
|
228
|
+
export function hasExplicitHostGhSelection(environ) {
|
|
229
|
+
return (environ[GITHUB_AUTH_MODE_ENV] ?? "").trim().toLowerCase() === HOST_GH_SELECTION;
|
|
230
|
+
}
|
|
231
|
+
/** Remediation naming the opt-in, for operator-visible not-ready output. */
|
|
232
|
+
export const EXPLICIT_SELECTION_REMEDIATION = `Remediation for an ambiguous Cursor runtime (#3859):\n` +
|
|
233
|
+
` - CURSOR_AGENT is set but the ${MANAGED_RUNTIME_SOCKET_ENV} metadata API did not report\n` +
|
|
234
|
+
` "${MANAGED_RUNTIME_VALUE}", so this runtime could be local desktop, a self-hosted\n` +
|
|
235
|
+
` worker, or an unreachable managed VM. Deft will not guess from the OS.\n` +
|
|
236
|
+
` - On a machine you control, where the host gh credential store is the right\n` +
|
|
237
|
+
` credential, opt in explicitly: set ${GITHUB_AUTH_MODE_ENV}=${HOST_GH_SELECTION}\n` +
|
|
238
|
+
` - Dispatchers should export the same github_auth_mode label they record in the\n` +
|
|
239
|
+
` dispatch envelope\n` +
|
|
240
|
+
` - A positive managed-runtime read always wins; this opt-in cannot override it`;
|
|
241
|
+
//# sourceMappingURL=cursor-managed-runtime.js.map
|
package/dist/platform/index.d.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
export * from "./agents-consumer-header.js";
|
|
2
2
|
export * from "./agents-md.js";
|
|
3
3
|
export * from "./constants.js";
|
|
4
|
+
export * from "./cursor-managed-runtime.js";
|
|
4
5
|
export * from "./host-content-surface.js";
|
|
5
6
|
export * from "./ip-risk.js";
|
|
6
7
|
export * from "./linear-scan.js";
|
package/dist/platform/index.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
export * from "./agents-consumer-header.js";
|
|
2
2
|
export * from "./agents-md.js";
|
|
3
3
|
export * from "./constants.js";
|
|
4
|
+
export * from "./cursor-managed-runtime.js";
|
|
4
5
|
export * from "./host-content-surface.js";
|
|
5
6
|
export * from "./ip-risk.js";
|
|
6
7
|
export * from "./linear-scan.js";
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { type ManagedRuntimeProbe } from "./cursor-managed-runtime.js";
|
|
1
2
|
import { type ShellContext } from "./shell-context.js";
|
|
2
3
|
export interface UidMapEntry {
|
|
3
4
|
readonly insideId: number;
|
|
@@ -14,6 +15,8 @@ export interface RuntimeCapabilityReport {
|
|
|
14
15
|
readonly hostPlatform: NodeJS.Platform;
|
|
15
16
|
readonly shell: ShellContext;
|
|
16
17
|
readonly runtimeMode: string;
|
|
18
|
+
/** Stable id naming why `runtimeMode` was chosen (#3859). */
|
|
19
|
+
readonly runtimeModeReason: string;
|
|
17
20
|
readonly identityKind: string;
|
|
18
21
|
readonly effectiveUid: number | null;
|
|
19
22
|
readonly effectiveUsername: string | null;
|
|
@@ -33,7 +36,24 @@ export declare function classifyIdentityKind(options: {
|
|
|
33
36
|
effectiveUid: number | null;
|
|
34
37
|
sandboxUidRemap: boolean;
|
|
35
38
|
}): string;
|
|
36
|
-
export
|
|
39
|
+
export interface RuntimeClassification {
|
|
40
|
+
readonly mode: string;
|
|
41
|
+
readonly reason: string;
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Classify runtime mode and name the reason for the verdict (#3859).
|
|
45
|
+
*
|
|
46
|
+
* Ordering matches intake/platform-capabilities.ts: CI markers, then a positive
|
|
47
|
+
* managed-runtime read, then sandbox, then the ambiguous Cursor hop. The Cursor
|
|
48
|
+
* hop previously ran first here; it now runs last so neither a CI marker nor a
|
|
49
|
+
* managed read can be overridden by an explicit host-gh selection.
|
|
50
|
+
*/
|
|
51
|
+
export declare function classifyRuntime(environ: Readonly<Record<string, string>>, sandboxUidRemap: boolean, options?: {
|
|
52
|
+
managedRuntimeProbe?: ManagedRuntimeProbe;
|
|
53
|
+
}): RuntimeClassification;
|
|
54
|
+
export declare function classifyRuntimeMode(environ: Readonly<Record<string, string>>, sandboxUidRemap: boolean, options?: {
|
|
55
|
+
managedRuntimeProbe?: ManagedRuntimeProbe;
|
|
56
|
+
}): string;
|
|
37
57
|
export interface ProbeRuntimeOptions {
|
|
38
58
|
readonly environ?: Readonly<Record<string, string>>;
|
|
39
59
|
readonly platform?: NodeJS.Platform;
|
|
@@ -43,6 +63,8 @@ export interface ProbeRuntimeOptions {
|
|
|
43
63
|
readonly effectiveUidOverride?: number | null;
|
|
44
64
|
readonly effectiveUsername?: string | null;
|
|
45
65
|
readonly getuid?: () => number;
|
|
66
|
+
/** Injectable managed-runtime probe (#3859). Defaults to the metadata read. */
|
|
67
|
+
readonly managedRuntimeProbe?: ManagedRuntimeProbe;
|
|
46
68
|
}
|
|
47
69
|
export declare function probeRuntimeCapabilities(options?: ProbeRuntimeOptions): RuntimeCapabilityReport;
|
|
48
70
|
export declare function getPlatformCapabilities(options?: ProbeRuntimeOptions): RuntimeCapabilityReport;
|