@deftai/directive-core 0.108.0 → 0.109.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cache/operations.js +1 -1
- package/dist/check/gate-lists.d.ts +14 -0
- package/dist/check/gate-lists.js +26 -3
- package/dist/check/session-completed-ac.d.ts +1 -1
- package/dist/check/session-completed-ac.js +1 -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/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 +7 -0
- package/dist/design-critique/citation-grammar.js +1 -1
- package/dist/design-critique/completed-arc-record.d.ts +8 -1
- package/dist/design-critique/completed-arc-record.js +98 -14
- package/dist/hooks/classify/host-session-identity.d.ts +25 -12
- package/dist/hooks/classify/host-session-identity.js +64 -49
- package/dist/hooks/classify/index.d.ts +2 -2
- package/dist/hooks/classify/index.js +2 -2
- package/dist/hooks/classify/paths.d.ts +2 -0
- package/dist/hooks/classify/paths.js +8 -4
- package/dist/hooks/classify/stdin.js +69 -3
- package/dist/hooks/dest-form.d.ts +20 -1
- package/dist/hooks/dest-form.js +158 -21
- package/dist/hooks/dispatcher.d.ts +22 -2
- package/dist/hooks/dispatcher.js +338 -66
- package/dist/hooks/fixtures/cases.d.ts +6 -0
- package/dist/hooks/fixtures/cases.js +53 -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 +3 -0
- package/dist/hooks/index.js +3 -0
- package/dist/hooks/owner-liveness.d.ts +92 -0
- package/dist/hooks/owner-liveness.js +103 -0
- 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 +49 -2
- package/dist/hooks/tools.js +96 -1
- package/dist/init-deposit/agent-hooks.d.ts +10 -0
- package/dist/init-deposit/agent-hooks.js +39 -0
- package/dist/init-deposit/gitignore.d.ts +7 -0
- package/dist/init-deposit/gitignore.js +24 -0
- package/dist/init-deposit/host-tool-coverage.d.ts +53 -0
- package/dist/init-deposit/host-tool-coverage.js +150 -0
- package/dist/init-deposit/index.d.ts +1 -0
- package/dist/init-deposit/index.js +1 -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/issue-ingest.d.ts +6 -1
- package/dist/intake/issue-ingest.js +21 -2
- 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/orchestration/subagent-monitor.d.ts +6 -0
- package/dist/orchestration/subagent-monitor.js +23 -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 +27 -0
- package/dist/orphan-active/evaluate.js +63 -6
- package/dist/orphan-active/index.d.ts +1 -0
- package/dist/orphan-active/index.js +1 -0
- package/dist/policy/merge-approval-head.js +7 -6
- 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.js +36 -9
- package/dist/pr-wait-mergeable/wrappers.js +3 -3
- package/dist/product-first-done-gate/evaluate.js +6 -2
- package/dist/product-first-done-gate/types.js +2 -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 +2 -0
- package/dist/release/index.js +2 -0
- package/dist/release/pipeline.js +55 -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 +6 -0
- package/dist/review-monitor/github-lease.js +4 -3
- 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 +41 -11
- package/dist/scm/gh-rest.d.ts +25 -12
- package/dist/scm/gh-rest.js +65 -15
- package/dist/scm/index.d.ts +2 -0
- package/dist/scm/index.js +2 -0
- 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/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/child-occupancy.d.ts +72 -0
- package/dist/session/child-occupancy.js +209 -0
- package/dist/session/host-session-owner.d.ts +93 -0
- package/dist/session/host-session-owner.js +148 -0
- package/dist/session/index.d.ts +2 -0
- package/dist/session/index.js +2 -0
- package/dist/session/occupancy.d.ts +107 -2
- package/dist/session/occupancy.js +272 -34
- package/dist/session/verify-ac-session-cache.d.ts +2 -2
- package/dist/session/verify-ac-session-cache.js +2 -2
- package/dist/swarm/complete-cohort.js +2 -0
- package/dist/swarm/pre-dispatch.js +2 -0
- package/dist/swarm/subagent-status-dir.d.ts +2 -1
- package/dist/swarm/subagent-status-dir.js +11 -2
- package/dist/swarm/worktrees.js +2 -0
- package/dist/triage/evaluate/worktrees.js +153 -6
- 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/verify-ac/evaluate.d.ts +9 -0
- package/dist/verify-ac/evaluate.js +32 -9
- package/dist/verify-env/agent-hooks.d.ts +6 -1
- package/dist/verify-env/agent-hooks.js +28 -2
- 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/semantic-single-source.d.ts +36 -0
- package/dist/verify-source/semantic-single-source.js +349 -0
- package/package.json +3 -3
|
@@ -702,6 +702,59 @@ export const HOOK_FIXTURE_CASES = [
|
|
|
702
702
|
hostIdentity: { status: "invalid", sessionId: null },
|
|
703
703
|
},
|
|
704
704
|
},
|
|
705
|
+
// --- Host-env session identity (#3873) ---
|
|
706
|
+
{
|
|
707
|
+
// The claim side of the loop: without this rewrite the lease is claimed
|
|
708
|
+
// under a minted UUID that the later write gate can never present.
|
|
709
|
+
id: "grok-posix-shell-host-env-lifecycle-rewrite",
|
|
710
|
+
host: "grok",
|
|
711
|
+
os: "posix",
|
|
712
|
+
tool: "Shell",
|
|
713
|
+
regression: ["#3873", "#3611"],
|
|
714
|
+
payload: {
|
|
715
|
+
tool_name: "Bash",
|
|
716
|
+
tool_input: { command: "deft session:start" },
|
|
717
|
+
},
|
|
718
|
+
environ: { GROK_SESSION_ID: "grok-session-a" },
|
|
719
|
+
expected: {
|
|
720
|
+
toolName: "Bash",
|
|
721
|
+
writeIntent: "shell",
|
|
722
|
+
writeTargetPath: null,
|
|
723
|
+
hostIdentity: {
|
|
724
|
+
status: "ok",
|
|
725
|
+
sessionId: "host:grok:v1:Z3Jvay1zZXNzaW9uLWE",
|
|
726
|
+
},
|
|
727
|
+
lifecycle: {
|
|
728
|
+
verb: "session:start",
|
|
729
|
+
requestedSessionId: "host:grok:v1:Z3Jvay1zZXNzaW9uLWE",
|
|
730
|
+
resultKind: "rewrite",
|
|
731
|
+
rewrittenCommand: "deft session:start --session-id=host:grok:v1:Z3Jvay1zZXNzaW9uLWE",
|
|
732
|
+
updatedInput: {
|
|
733
|
+
command: "deft session:start --session-id=host:grok:v1:Z3Jvay1zZXNzaW9uLWE",
|
|
734
|
+
},
|
|
735
|
+
},
|
|
736
|
+
},
|
|
737
|
+
},
|
|
738
|
+
{
|
|
739
|
+
id: "grok-posix-write-payload-session-id-untrusted",
|
|
740
|
+
host: "grok",
|
|
741
|
+
os: "posix",
|
|
742
|
+
tool: "Write",
|
|
743
|
+
regression: ["#3873"],
|
|
744
|
+
payload: {
|
|
745
|
+
tool_name: "Write",
|
|
746
|
+
session_id: "payload-owner",
|
|
747
|
+
tool_input: { content: "x", file_path: "src/app.ts" },
|
|
748
|
+
},
|
|
749
|
+
environ: {},
|
|
750
|
+
expected: {
|
|
751
|
+
toolName: "Write",
|
|
752
|
+
writeIntent: "direct-write",
|
|
753
|
+
writeTargetPath: "src/app.ts",
|
|
754
|
+
hostIdentity: { status: "missing", sessionId: null },
|
|
755
|
+
},
|
|
756
|
+
notes: "Grok's payload session_id contract is unverified; only the host variable is read",
|
|
757
|
+
},
|
|
705
758
|
// --- Regression: empty / missing tool name ---
|
|
706
759
|
{
|
|
707
760
|
id: "cursor-posix-stdin-empty",
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Durable attribution log for tree-wide destructive git at the write fence (#3917).
|
|
3
|
+
*
|
|
4
|
+
* Lives outside the repository so a `reset --hard` or a worktree prune cannot
|
|
5
|
+
* erase it. This is detection, not a root-cause claim.
|
|
6
|
+
*/
|
|
7
|
+
import type { GitDestructiveKind } from "./dest-form.js";
|
|
8
|
+
export declare const GIT_DESTRUCTIVE_LOG_ENV = "DEFT_GIT_DESTRUCTIVE_LOG";
|
|
9
|
+
export type GitDestructiveDisposition = "deny" | "allow-fixture";
|
|
10
|
+
export interface GitDestructiveRecord {
|
|
11
|
+
readonly ts: string;
|
|
12
|
+
readonly kind: GitDestructiveKind;
|
|
13
|
+
readonly disposition: GitDestructiveDisposition;
|
|
14
|
+
readonly command: string;
|
|
15
|
+
readonly projectRoot: string;
|
|
16
|
+
readonly host: string;
|
|
17
|
+
readonly toolName: string | null;
|
|
18
|
+
readonly actor: string;
|
|
19
|
+
readonly pid: number;
|
|
20
|
+
readonly relocators: readonly string[];
|
|
21
|
+
readonly unprovable: boolean;
|
|
22
|
+
}
|
|
23
|
+
export declare function resolveGitDestructiveLogPath(env?: NodeJS.ProcessEnv, platform?: NodeJS.Platform, homeDir?: string): string;
|
|
24
|
+
/**
|
|
25
|
+
* Best-effort append. A log failure must not block the deny.
|
|
26
|
+
*/
|
|
27
|
+
export declare function appendGitDestructiveRecord(record: GitDestructiveRecord, options?: {
|
|
28
|
+
readonly logPath?: string;
|
|
29
|
+
readonly env?: NodeJS.ProcessEnv;
|
|
30
|
+
readonly append?: (path: string, line: string) => void;
|
|
31
|
+
}): void;
|
|
32
|
+
//# sourceMappingURL=git-destructive-log.d.ts.map
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Durable attribution log for tree-wide destructive git at the write fence (#3917).
|
|
3
|
+
*
|
|
4
|
+
* Lives outside the repository so a `reset --hard` or a worktree prune cannot
|
|
5
|
+
* erase it. This is detection, not a root-cause claim.
|
|
6
|
+
*/
|
|
7
|
+
import { mkdirSync } from "node:fs";
|
|
8
|
+
import { homedir } from "node:os";
|
|
9
|
+
import { dirname, join, resolve } from "node:path";
|
|
10
|
+
import { containedWrite } from "../fs/contained-write.js";
|
|
11
|
+
import { platformUserConfigDir } from "../user-config/resolve-user-md.js";
|
|
12
|
+
export const GIT_DESTRUCTIVE_LOG_ENV = "DEFT_GIT_DESTRUCTIVE_LOG";
|
|
13
|
+
export function resolveGitDestructiveLogPath(env = process.env, platform = process.platform, homeDir = homedir()) {
|
|
14
|
+
const override = env[GIT_DESTRUCTIVE_LOG_ENV]?.trim();
|
|
15
|
+
if (override !== undefined && override.length > 0) {
|
|
16
|
+
return resolve(override);
|
|
17
|
+
}
|
|
18
|
+
return join(platformUserConfigDir(platform, env, homeDir), "logs", "git-destructive.jsonl");
|
|
19
|
+
}
|
|
20
|
+
function defaultAppend(path, line) {
|
|
21
|
+
const targetAbs = resolve(path);
|
|
22
|
+
const parent = dirname(targetAbs);
|
|
23
|
+
mkdirSync(parent, { recursive: true });
|
|
24
|
+
containedWrite({
|
|
25
|
+
root: parent,
|
|
26
|
+
target: targetAbs,
|
|
27
|
+
data: line,
|
|
28
|
+
mode: "append",
|
|
29
|
+
mkdir: false,
|
|
30
|
+
mutation: false,
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Best-effort append. A log failure must not block the deny.
|
|
35
|
+
*/
|
|
36
|
+
export function appendGitDestructiveRecord(record, options = {}) {
|
|
37
|
+
try {
|
|
38
|
+
const path = options.logPath ?? resolveGitDestructiveLogPath(options.env ?? process.env);
|
|
39
|
+
const line = `${JSON.stringify(record)}\n`;
|
|
40
|
+
(options.append ?? defaultAppend)(path, line);
|
|
41
|
+
}
|
|
42
|
+
catch {
|
|
43
|
+
// Attribution is best-effort; the fail-closed deny still stands.
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
//# sourceMappingURL=git-destructive-log.js.map
|
package/dist/hooks/index.d.ts
CHANGED
|
@@ -3,5 +3,8 @@ export * from "./cursor-hooks.js";
|
|
|
3
3
|
export * from "./dest-form.js";
|
|
4
4
|
export * from "./dispatcher.js";
|
|
5
5
|
export * from "./fixtures/index.js";
|
|
6
|
+
export * from "./git-destructive-log.js";
|
|
7
|
+
export * from "./owner-liveness.js";
|
|
6
8
|
export * from "./scope.js";
|
|
9
|
+
export * from "./shell-write-targets.js";
|
|
7
10
|
//# sourceMappingURL=index.d.ts.map
|
package/dist/hooks/index.js
CHANGED
|
@@ -3,5 +3,8 @@ export * from "./cursor-hooks.js";
|
|
|
3
3
|
export * from "./dest-form.js";
|
|
4
4
|
export * from "./dispatcher.js";
|
|
5
5
|
export * from "./fixtures/index.js";
|
|
6
|
+
export * from "./git-destructive-log.js";
|
|
7
|
+
export * from "./owner-liveness.js";
|
|
6
8
|
export * from "./scope.js";
|
|
9
|
+
export * from "./shell-write-targets.js";
|
|
7
10
|
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Owner liveness on non-write hook activity (#3987).
|
|
3
|
+
*
|
|
4
|
+
* The occupancy lease renews on one signal: a gated product write
|
|
5
|
+
* (`evaluateOccupancyWriteGate`, refresh path, #3599). After #3990 widened the
|
|
6
|
+
* PreToolUse matchers, coverage is necessary and still not sufficient, because
|
|
7
|
+
* eligibility — not coverage — is what keeps shell traffic off that path. Only
|
|
8
|
+
* five write verbs are recognized, and a dest carrying `$` / `*` / `?`, a temp
|
|
9
|
+
* path, an out-of-root target, or any compound command is refused before the
|
|
10
|
+
* gate re-stamps. `cd <root>; <command>` is compound, and the mandated Windows
|
|
11
|
+
* body-file flow (#2646 / #2744) is compound, `$`-bearing and temp-targeted at
|
|
12
|
+
* once. So an owner can commit and push all session and still starve its own
|
|
13
|
+
* lease, which is the data-loss half of #3987.
|
|
14
|
+
*
|
|
15
|
+
* The remedy is the one `occupancy.ts` already names in the
|
|
16
|
+
* `OCCUPANCY_MAX_LEASE_MS` docblock: "refresh on non-write activity". The hook
|
|
17
|
+
* fires on every matched tool call and resolves the actor from the host
|
|
18
|
+
* payload, so the owner's presence is already proven there — no new identity
|
|
19
|
+
* has to travel anywhere.
|
|
20
|
+
*
|
|
21
|
+
* Bounds, each one load-bearing (5471374558 F4):
|
|
22
|
+
*
|
|
23
|
+
* - Host-authoritative actor only. An ambient inherited `DEFT_SESSION_ID` is
|
|
24
|
+
* never enough. On Grok the owner id is a hook-sibling variable, and
|
|
25
|
+
* publishing it so an agent shell could present it would hand lease renewal
|
|
26
|
+
* to every descendant process — the widening the arc withdrew.
|
|
27
|
+
* - Owner only, via `heartbeatOccupancy`: it never claims, never mints, never
|
|
28
|
+
* resurrects, and refuses a foreign or expired lease. Reusing it keeps one
|
|
29
|
+
* renewal path with one set of refusal semantics.
|
|
30
|
+
* - `markWrite` stays false, so `last_write_at` still means "a product write
|
|
31
|
+
* was recorded" and `no recorded write` stays honest for a would-be stealer.
|
|
32
|
+
* - Keyed on the lease's own worktree path, so a record describing another tree
|
|
33
|
+
* is not renewed from this one. The caller supplies the tree the mutation
|
|
34
|
+
* gates authorized against — a linked worktree, not the payload root — so a
|
|
35
|
+
* worktree write renews the worktree's lease rather than the primary's.
|
|
36
|
+
* - `claimed_at` is untouched, so `OCCUPANCY_MAX_LEASE_MS` — 12 hours — is
|
|
37
|
+
* unmoved. Liveness renewal cannot outlive the absolute cap.
|
|
38
|
+
* - Runs after the decision, never as an input to it. A liveness re-stamp must
|
|
39
|
+
* not change any verdict, and re-stamping before the mutation gates would
|
|
40
|
+
* suppress their own `markWrite = true` refresh by resetting the age floor.
|
|
41
|
+
*
|
|
42
|
+
* Bound this deliberately does not cross: a call with no write target proves
|
|
43
|
+
* only the tree the host named. `projectRootFromHookPayload` takes that from
|
|
44
|
+
* the payload's own `cwd`-class fields, so a session working inside a linked
|
|
45
|
+
* worktree names the worktree and renews it; a session whose host reports the
|
|
46
|
+
* primary checkout renews the primary. When those differ and nothing in the
|
|
47
|
+
* payload says which tree the work is in, this renews neither by guessing —
|
|
48
|
+
* keeping a lease alive for a tree nobody occupies is a widening, not a fix,
|
|
49
|
+
* and the TTL reclaiming an unused tree is the behaviour the lease exists for.
|
|
50
|
+
* `deft occupancy:heartbeat --session-id <owner>` stays the explicit path for
|
|
51
|
+
* a session whose tree the host does not report.
|
|
52
|
+
*/
|
|
53
|
+
import type { LockDeps } from "../slice/lock.js";
|
|
54
|
+
export type OwnerLivenessSkipReason =
|
|
55
|
+
/** No host-authoritative owner resolved; ambient identity is not accepted. */
|
|
56
|
+
"no-host-authoritative-owner"
|
|
57
|
+
/** Nothing to renew: no lease, or the lease is expired or age-capped. */
|
|
58
|
+
| "no-live-lease"
|
|
59
|
+
/** A lease exists but this actor is not its owner. */
|
|
60
|
+
| "not-owner"
|
|
61
|
+
/** The lease record describes a different worktree than the one hooked. */
|
|
62
|
+
| "foreign-worktree"
|
|
63
|
+
/** Younger than the shared re-stamp floor; renewing would rewrite per keystroke. */
|
|
64
|
+
| "within-refresh-floor"
|
|
65
|
+
/** The lock was busy or the lease changed under us; the lease is left alone. */
|
|
66
|
+
| "refresh-unavailable";
|
|
67
|
+
export type OwnerLivenessOutcome = {
|
|
68
|
+
readonly restamped: true;
|
|
69
|
+
readonly sessionId: string;
|
|
70
|
+
readonly heartbeatAt: Date;
|
|
71
|
+
} | {
|
|
72
|
+
readonly restamped: false;
|
|
73
|
+
readonly reason: OwnerLivenessSkipReason;
|
|
74
|
+
};
|
|
75
|
+
export interface OwnerLivenessInput {
|
|
76
|
+
/** Tree the hook fired against; also the tree whose lease may be renewed. */
|
|
77
|
+
readonly projectRoot: string;
|
|
78
|
+
/** Owner id the host payload resolved to, or undefined when none did. */
|
|
79
|
+
readonly ownerSessionId: string | undefined;
|
|
80
|
+
/** False for ambient/environment identity — such an actor never renews. */
|
|
81
|
+
readonly hostAuthoritative: boolean;
|
|
82
|
+
readonly now?: Date;
|
|
83
|
+
readonly lockDeps?: LockDeps;
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* Renew the owner's lease from a hook event that proved the owner is alive.
|
|
87
|
+
*
|
|
88
|
+
* Pure side effect on `occupancy.json`: the caller's decision is already made
|
|
89
|
+
* and this never alters it. Every refusal path leaves the lease untouched.
|
|
90
|
+
*/
|
|
91
|
+
export declare function restampOwnerLivenessOnHookEvent(input: OwnerLivenessInput): OwnerLivenessOutcome;
|
|
92
|
+
//# sourceMappingURL=owner-liveness.d.ts.map
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Owner liveness on non-write hook activity (#3987).
|
|
3
|
+
*
|
|
4
|
+
* The occupancy lease renews on one signal: a gated product write
|
|
5
|
+
* (`evaluateOccupancyWriteGate`, refresh path, #3599). After #3990 widened the
|
|
6
|
+
* PreToolUse matchers, coverage is necessary and still not sufficient, because
|
|
7
|
+
* eligibility — not coverage — is what keeps shell traffic off that path. Only
|
|
8
|
+
* five write verbs are recognized, and a dest carrying `$` / `*` / `?`, a temp
|
|
9
|
+
* path, an out-of-root target, or any compound command is refused before the
|
|
10
|
+
* gate re-stamps. `cd <root>; <command>` is compound, and the mandated Windows
|
|
11
|
+
* body-file flow (#2646 / #2744) is compound, `$`-bearing and temp-targeted at
|
|
12
|
+
* once. So an owner can commit and push all session and still starve its own
|
|
13
|
+
* lease, which is the data-loss half of #3987.
|
|
14
|
+
*
|
|
15
|
+
* The remedy is the one `occupancy.ts` already names in the
|
|
16
|
+
* `OCCUPANCY_MAX_LEASE_MS` docblock: "refresh on non-write activity". The hook
|
|
17
|
+
* fires on every matched tool call and resolves the actor from the host
|
|
18
|
+
* payload, so the owner's presence is already proven there — no new identity
|
|
19
|
+
* has to travel anywhere.
|
|
20
|
+
*
|
|
21
|
+
* Bounds, each one load-bearing (5471374558 F4):
|
|
22
|
+
*
|
|
23
|
+
* - Host-authoritative actor only. An ambient inherited `DEFT_SESSION_ID` is
|
|
24
|
+
* never enough. On Grok the owner id is a hook-sibling variable, and
|
|
25
|
+
* publishing it so an agent shell could present it would hand lease renewal
|
|
26
|
+
* to every descendant process — the widening the arc withdrew.
|
|
27
|
+
* - Owner only, via `heartbeatOccupancy`: it never claims, never mints, never
|
|
28
|
+
* resurrects, and refuses a foreign or expired lease. Reusing it keeps one
|
|
29
|
+
* renewal path with one set of refusal semantics.
|
|
30
|
+
* - `markWrite` stays false, so `last_write_at` still means "a product write
|
|
31
|
+
* was recorded" and `no recorded write` stays honest for a would-be stealer.
|
|
32
|
+
* - Keyed on the lease's own worktree path, so a record describing another tree
|
|
33
|
+
* is not renewed from this one. The caller supplies the tree the mutation
|
|
34
|
+
* gates authorized against — a linked worktree, not the payload root — so a
|
|
35
|
+
* worktree write renews the worktree's lease rather than the primary's.
|
|
36
|
+
* - `claimed_at` is untouched, so `OCCUPANCY_MAX_LEASE_MS` — 12 hours — is
|
|
37
|
+
* unmoved. Liveness renewal cannot outlive the absolute cap.
|
|
38
|
+
* - Runs after the decision, never as an input to it. A liveness re-stamp must
|
|
39
|
+
* not change any verdict, and re-stamping before the mutation gates would
|
|
40
|
+
* suppress their own `markWrite = true` refresh by resetting the age floor.
|
|
41
|
+
*
|
|
42
|
+
* Bound this deliberately does not cross: a call with no write target proves
|
|
43
|
+
* only the tree the host named. `projectRootFromHookPayload` takes that from
|
|
44
|
+
* the payload's own `cwd`-class fields, so a session working inside a linked
|
|
45
|
+
* worktree names the worktree and renews it; a session whose host reports the
|
|
46
|
+
* primary checkout renews the primary. When those differ and nothing in the
|
|
47
|
+
* payload says which tree the work is in, this renews neither by guessing —
|
|
48
|
+
* keeping a lease alive for a tree nobody occupies is a widening, not a fix,
|
|
49
|
+
* and the TTL reclaiming an unused tree is the behaviour the lease exists for.
|
|
50
|
+
* `deft occupancy:heartbeat --session-id <owner>` stays the explicit path for
|
|
51
|
+
* a session whose tree the host does not report.
|
|
52
|
+
*/
|
|
53
|
+
import { resolve } from "node:path";
|
|
54
|
+
import { heartbeatOccupancy, isOccupancyExpired, OCCUPANCY_REFRESH_AFTER_MS, readOccupancy, } from "../session/occupancy.js";
|
|
55
|
+
function skip(reason) {
|
|
56
|
+
return { restamped: false, reason };
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* Renew the owner's lease from a hook event that proved the owner is alive.
|
|
60
|
+
*
|
|
61
|
+
* Pure side effect on `occupancy.json`: the caller's decision is already made
|
|
62
|
+
* and this never alters it. Every refusal path leaves the lease untouched.
|
|
63
|
+
*/
|
|
64
|
+
export function restampOwnerLivenessOnHookEvent(input) {
|
|
65
|
+
if (!input.hostAuthoritative)
|
|
66
|
+
return skip("no-host-authoritative-owner");
|
|
67
|
+
const owner = input.ownerSessionId?.trim() ?? "";
|
|
68
|
+
if (owner.length === 0)
|
|
69
|
+
return skip("no-host-authoritative-owner");
|
|
70
|
+
const root = resolve(input.projectRoot);
|
|
71
|
+
const now = input.now ?? new Date();
|
|
72
|
+
const record = readOccupancy(root);
|
|
73
|
+
if (record === null || isOccupancyExpired(record, now))
|
|
74
|
+
return skip("no-live-lease");
|
|
75
|
+
// Owner only. A granted member's presence is not the owner's presence, and
|
|
76
|
+
// `heartbeatOccupancy` refuses it anyway — checking here keeps the reason honest.
|
|
77
|
+
if (record.sessionId !== owner)
|
|
78
|
+
return skip("not-owner");
|
|
79
|
+
if (resolve(record.worktreePath) !== root)
|
|
80
|
+
return skip("foreign-worktree");
|
|
81
|
+
// Same floor the write gate uses, for the same reason: the hook runs on a
|
|
82
|
+
// large fraction of tool calls, so an unconditional renew would rewrite the
|
|
83
|
+
// lease file continuously without lengthening the safe window at all.
|
|
84
|
+
if (now.getTime() - record.heartbeatAt.getTime() < OCCUPANCY_REFRESH_AFTER_MS) {
|
|
85
|
+
return skip("within-refresh-floor");
|
|
86
|
+
}
|
|
87
|
+
const beat = heartbeatOccupancy(root, {
|
|
88
|
+
sessionId: owner,
|
|
89
|
+
// Never fall back to an ambient owner: the resolved host identity is the
|
|
90
|
+
// only thing that proves who is here.
|
|
91
|
+
env: {},
|
|
92
|
+
now,
|
|
93
|
+
lockDeps: input.lockDeps,
|
|
94
|
+
});
|
|
95
|
+
if (beat.code !== 0 || beat.record === null)
|
|
96
|
+
return skip("refresh-unavailable");
|
|
97
|
+
return {
|
|
98
|
+
restamped: true,
|
|
99
|
+
sessionId: beat.record.sessionId,
|
|
100
|
+
heartbeatAt: beat.record.heartbeatAt,
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
//# sourceMappingURL=owner-liveness.js.map
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export declare const SHELL_WRITE_KINDS: readonly ["set-content", "out-file", "add-content", "python-pathlib", "io-writealltext"];
|
|
2
|
+
export type ShellWriteKind = (typeof SHELL_WRITE_KINDS)[number];
|
|
3
|
+
export interface ShellWriteTarget {
|
|
4
|
+
readonly kind: ShellWriteKind;
|
|
5
|
+
readonly path: string;
|
|
6
|
+
readonly unprovable?: boolean;
|
|
7
|
+
}
|
|
8
|
+
export declare function classifyShellWriteTargets(command: string): ShellWriteTarget[];
|
|
9
|
+
export declare function isInRepoShellWritePath(projectRoot: string, dest: string): boolean;
|
|
10
|
+
//# sourceMappingURL=shell-write-targets.d.ts.map
|
|
@@ -0,0 +1,274 @@
|
|
|
1
|
+
/** Recognized in-repo Shell file-write dests (#3983 / #3987). */
|
|
2
|
+
import { isAbsolute, relative, resolve } from "node:path";
|
|
3
|
+
export const SHELL_WRITE_KINDS = [
|
|
4
|
+
"set-content",
|
|
5
|
+
"out-file",
|
|
6
|
+
"add-content",
|
|
7
|
+
"python-pathlib",
|
|
8
|
+
"io-writealltext",
|
|
9
|
+
];
|
|
10
|
+
const DQ = String.fromCharCode(34);
|
|
11
|
+
const AQ = String.fromCharCode(39);
|
|
12
|
+
function stripQuotes(token) {
|
|
13
|
+
const t = token.trim();
|
|
14
|
+
if (t.length < 2)
|
|
15
|
+
return t;
|
|
16
|
+
const a = t[0];
|
|
17
|
+
const b = t[t.length - 1];
|
|
18
|
+
if ((a === DQ && b === DQ) || (a === AQ && b === AQ))
|
|
19
|
+
return t.slice(1, -1);
|
|
20
|
+
return t;
|
|
21
|
+
}
|
|
22
|
+
function pushUnique(out, kind, raw) {
|
|
23
|
+
const path = stripQuotes(raw).trim();
|
|
24
|
+
if (path.length === 0)
|
|
25
|
+
return;
|
|
26
|
+
if (out.some((d) => d.kind === kind && d.path === path))
|
|
27
|
+
return;
|
|
28
|
+
out.push({ kind, path });
|
|
29
|
+
}
|
|
30
|
+
function extractQuoted(command, start) {
|
|
31
|
+
if (start >= command.length)
|
|
32
|
+
return { value: "", next: start };
|
|
33
|
+
const q = command[start];
|
|
34
|
+
if (q !== DQ && q !== AQ) {
|
|
35
|
+
let i = start;
|
|
36
|
+
while (i < command.length) {
|
|
37
|
+
const c = command[i];
|
|
38
|
+
if (c === undefined)
|
|
39
|
+
break;
|
|
40
|
+
if (c === " " || c === " " || c === "," || c === ")")
|
|
41
|
+
break;
|
|
42
|
+
i += 1;
|
|
43
|
+
}
|
|
44
|
+
return { value: command.slice(start, i), next: i };
|
|
45
|
+
}
|
|
46
|
+
let i = start + 1;
|
|
47
|
+
while (i < command.length && command[i] !== q)
|
|
48
|
+
i += 1;
|
|
49
|
+
return { value: command.slice(start + 1, i), next: i + 1 };
|
|
50
|
+
}
|
|
51
|
+
function skipWs(command, start) {
|
|
52
|
+
let i = start;
|
|
53
|
+
while (i < command.length) {
|
|
54
|
+
const code = command.charCodeAt(i);
|
|
55
|
+
if (code !== 32 && code !== 9 && code !== 10 && code !== 13)
|
|
56
|
+
break;
|
|
57
|
+
i += 1;
|
|
58
|
+
}
|
|
59
|
+
return i;
|
|
60
|
+
}
|
|
61
|
+
function startsWithVerb(segment, verb) {
|
|
62
|
+
const i = skipWs(segment, 0);
|
|
63
|
+
if (segment.slice(i, i + verb.length).toLowerCase() !== verb.toLowerCase())
|
|
64
|
+
return false;
|
|
65
|
+
const next = segment[i + verb.length];
|
|
66
|
+
return next === undefined || next === " " || next === " " || next === "-";
|
|
67
|
+
}
|
|
68
|
+
function splitWriteSegments(command) {
|
|
69
|
+
const segs = [];
|
|
70
|
+
let cur = "";
|
|
71
|
+
let quote = null;
|
|
72
|
+
for (let i = 0; i < command.length; i++) {
|
|
73
|
+
const c = command[i];
|
|
74
|
+
if (c === undefined)
|
|
75
|
+
break;
|
|
76
|
+
if (quote !== null) {
|
|
77
|
+
if (c === quote)
|
|
78
|
+
quote = null;
|
|
79
|
+
cur += c;
|
|
80
|
+
continue;
|
|
81
|
+
}
|
|
82
|
+
if (c === DQ || c === AQ) {
|
|
83
|
+
quote = c;
|
|
84
|
+
cur += c;
|
|
85
|
+
continue;
|
|
86
|
+
}
|
|
87
|
+
const code = command.charCodeAt(i);
|
|
88
|
+
const next = command.charCodeAt(i + 1);
|
|
89
|
+
if ((code === 38 && next === 38) || (code === 124 && next === 124)) {
|
|
90
|
+
segs.push(cur);
|
|
91
|
+
cur = "";
|
|
92
|
+
i += 1;
|
|
93
|
+
continue;
|
|
94
|
+
}
|
|
95
|
+
if (code === 124 || code === 38 || code === 59 || code === 10 || code === 13) {
|
|
96
|
+
segs.push(cur);
|
|
97
|
+
cur = "";
|
|
98
|
+
continue;
|
|
99
|
+
}
|
|
100
|
+
cur += c;
|
|
101
|
+
}
|
|
102
|
+
segs.push(cur);
|
|
103
|
+
return segs.map((seg) => seg.trim()).filter((seg) => seg.length > 0);
|
|
104
|
+
}
|
|
105
|
+
const PATH_PARAM_NAMES = ["path", "literalpath", "filepath"];
|
|
106
|
+
function uniqueNamedParam(flag, names) {
|
|
107
|
+
const f = flag.replace(/^-+/, "").toLowerCase();
|
|
108
|
+
if (f.length === 0)
|
|
109
|
+
return null;
|
|
110
|
+
const hits = names.filter((n) => n.startsWith(f));
|
|
111
|
+
return hits.length === 1 ? (hits[0] ?? null) : null;
|
|
112
|
+
}
|
|
113
|
+
function parseFlag(token) {
|
|
114
|
+
if (!token.startsWith("-"))
|
|
115
|
+
return null;
|
|
116
|
+
const colon = token.indexOf(":");
|
|
117
|
+
if (colon <= 0)
|
|
118
|
+
return { flag: token, attached: null };
|
|
119
|
+
return { flag: token.slice(0, colon), attached: token.slice(colon + 1) };
|
|
120
|
+
}
|
|
121
|
+
function extractAfterVerb(command, verb, kind, out) {
|
|
122
|
+
if (!startsWithVerb(command, verb))
|
|
123
|
+
return;
|
|
124
|
+
const i = skipWs(command, 0);
|
|
125
|
+
let j = skipWs(command, i + verb.length);
|
|
126
|
+
let named = null;
|
|
127
|
+
let positional = null;
|
|
128
|
+
while (j < command.length) {
|
|
129
|
+
const extracted = extractQuoted(command, j);
|
|
130
|
+
if (extracted.value.length === 0)
|
|
131
|
+
break;
|
|
132
|
+
j = skipWs(command, extracted.next);
|
|
133
|
+
const parsed = parseFlag(extracted.value);
|
|
134
|
+
if (parsed === null) {
|
|
135
|
+
if (positional === null)
|
|
136
|
+
positional = extracted.value;
|
|
137
|
+
continue;
|
|
138
|
+
}
|
|
139
|
+
let arg = parsed.attached;
|
|
140
|
+
if (arg === null || arg.length === 0) {
|
|
141
|
+
const next = extractQuoted(command, j);
|
|
142
|
+
if (next.value.length > 0 && !next.value.startsWith("-")) {
|
|
143
|
+
arg = next.value;
|
|
144
|
+
j = skipWs(command, next.next);
|
|
145
|
+
}
|
|
146
|
+
else {
|
|
147
|
+
arg = null;
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
if (uniqueNamedParam(parsed.flag, PATH_PARAM_NAMES) !== null) {
|
|
151
|
+
if (arg !== null && arg.length > 0 && !arg.startsWith("-"))
|
|
152
|
+
named = arg;
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
const dest = named ?? positional;
|
|
156
|
+
if (dest !== null && !dest.startsWith("-"))
|
|
157
|
+
pushUnique(out, kind, dest);
|
|
158
|
+
}
|
|
159
|
+
function pathlibWriteFollows(command, afterPathArgs) {
|
|
160
|
+
let i = skipWs(command, afterPathArgs);
|
|
161
|
+
if (command[i] === ")")
|
|
162
|
+
i = skipWs(command, i + 1);
|
|
163
|
+
while (i < command.length) {
|
|
164
|
+
if (command[i] === ";")
|
|
165
|
+
return false;
|
|
166
|
+
i = skipWs(command, i);
|
|
167
|
+
if (command[i] !== ".")
|
|
168
|
+
return false;
|
|
169
|
+
i += 1;
|
|
170
|
+
const start = i;
|
|
171
|
+
while (i < command.length) {
|
|
172
|
+
const c = command.charCodeAt(i);
|
|
173
|
+
if ((c >= 65 && c <= 90) || (c >= 97 && c <= 122) || c === 95) {
|
|
174
|
+
i += 1;
|
|
175
|
+
continue;
|
|
176
|
+
}
|
|
177
|
+
break;
|
|
178
|
+
}
|
|
179
|
+
const name = command.slice(start, i);
|
|
180
|
+
i = skipWs(command, i);
|
|
181
|
+
if (command[i] !== "(")
|
|
182
|
+
return false;
|
|
183
|
+
if (name === "write_text")
|
|
184
|
+
return true;
|
|
185
|
+
if (name === "write_bytes")
|
|
186
|
+
return true;
|
|
187
|
+
i += 1;
|
|
188
|
+
let depth = 1;
|
|
189
|
+
while (i < command.length && depth > 0) {
|
|
190
|
+
const ch = command[i];
|
|
191
|
+
if (ch === "(")
|
|
192
|
+
depth += 1;
|
|
193
|
+
else if (ch === ")")
|
|
194
|
+
depth -= 1;
|
|
195
|
+
i += 1;
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
return false;
|
|
199
|
+
}
|
|
200
|
+
function extractPythonPathlib(command, out) {
|
|
201
|
+
const i = skipWs(command, 0);
|
|
202
|
+
const head = command.slice(i, i + 10).toLowerCase();
|
|
203
|
+
if (!head.startsWith("python") && !head.startsWith("py ") && !head.startsWith("py.exe"))
|
|
204
|
+
return;
|
|
205
|
+
const needle = "Path(";
|
|
206
|
+
let from = i;
|
|
207
|
+
while (from < command.length) {
|
|
208
|
+
const at = command.indexOf(needle, from);
|
|
209
|
+
if (at < 0)
|
|
210
|
+
return;
|
|
211
|
+
let j = skipWs(command, at + needle.length);
|
|
212
|
+
const ch = command[j];
|
|
213
|
+
if (ch === "r" || ch === "R")
|
|
214
|
+
j = skipWs(command, j + 1);
|
|
215
|
+
const extracted = extractQuoted(command, j);
|
|
216
|
+
if (pathlibWriteFollows(command, extracted.next)) {
|
|
217
|
+
pushUnique(out, "python-pathlib", extracted.value);
|
|
218
|
+
}
|
|
219
|
+
from = at + needle.length;
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
function extractIoWrite(command, out) {
|
|
223
|
+
const i = skipWs(command, 0);
|
|
224
|
+
if (command[i] !== "[")
|
|
225
|
+
return;
|
|
226
|
+
const needles = ["WriteAllText(", "WriteAllBytes("];
|
|
227
|
+
for (const needle of needles) {
|
|
228
|
+
const at = command.indexOf(needle, i);
|
|
229
|
+
if (at < 0)
|
|
230
|
+
continue;
|
|
231
|
+
const j = skipWs(command, at + needle.length);
|
|
232
|
+
const extracted = extractQuoted(command, j);
|
|
233
|
+
pushUnique(out, "io-writealltext", extracted.value);
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
function classifySimpleWrite(command) {
|
|
237
|
+
const out = [];
|
|
238
|
+
extractAfterVerb(command, "Set-Content", "set-content", out);
|
|
239
|
+
extractAfterVerb(command, "Add-Content", "add-content", out);
|
|
240
|
+
extractAfterVerb(command, "Out-File", "out-file", out);
|
|
241
|
+
extractPythonPathlib(command, out);
|
|
242
|
+
extractIoWrite(command, out);
|
|
243
|
+
return out;
|
|
244
|
+
}
|
|
245
|
+
export function classifyShellWriteTargets(command) {
|
|
246
|
+
const cmd = command.trim();
|
|
247
|
+
if (cmd.length === 0)
|
|
248
|
+
return [];
|
|
249
|
+
const segments = splitWriteSegments(cmd);
|
|
250
|
+
if (segments.length === 1)
|
|
251
|
+
return classifySimpleWrite(segments[0] ?? cmd);
|
|
252
|
+
const found = [];
|
|
253
|
+
for (const seg of segments)
|
|
254
|
+
found.push(...classifySimpleWrite(seg));
|
|
255
|
+
if (found.length === 0)
|
|
256
|
+
return [];
|
|
257
|
+
return found.map((d) => ({ ...d, unprovable: true }));
|
|
258
|
+
}
|
|
259
|
+
export function isInRepoShellWritePath(projectRoot, dest) {
|
|
260
|
+
const path = dest.trim();
|
|
261
|
+
if (path.length === 0)
|
|
262
|
+
return false;
|
|
263
|
+
if (path.includes("*") || path.includes("?") || path.includes("$"))
|
|
264
|
+
return false;
|
|
265
|
+
const root = resolve(projectRoot);
|
|
266
|
+
const abs = isAbsolute(path) ? resolve(path) : resolve(root, path);
|
|
267
|
+
const rel = relative(root, abs);
|
|
268
|
+
if (rel.length === 0)
|
|
269
|
+
return false;
|
|
270
|
+
if (rel.startsWith("..") || isAbsolute(rel))
|
|
271
|
+
return false;
|
|
272
|
+
return true;
|
|
273
|
+
}
|
|
274
|
+
//# sourceMappingURL=shell-write-targets.js.map
|