@yagni-app/code-staging 1.0.7-staging.1268.1 → 1.0.7-staging.1270.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/extension/hooks.d.ts +3 -3
- package/dist/extension/hooks.js +30 -5
- package/dist/extension/index.js +31 -36
- package/dist/extension/permission/gate.d.ts +5 -1
- package/dist/extension/permission/gate.js +19 -13
- package/dist/extension/permissionRules/loadConfig.d.ts +23 -12
- package/dist/extension/permissionRules/loadConfig.js +29 -14
- package/dist/extension/permissionRules/pathRules.d.ts +9 -7
- package/dist/extension/permissionRules/pathRules.js +10 -8
- package/dist/extension/sandbox/config.d.ts +15 -14
- package/dist/extension/sandbox/config.js +62 -40
- package/dist/extension/sandbox/manager.d.ts +10 -0
- package/dist/extension/sandbox/manager.js +28 -1
- package/dist/extension/sandbox/session.js +150 -96
- package/dist/extension/settingsFiles.d.ts +50 -0
- package/dist/extension/settingsFiles.js +206 -0
- package/package.json +2 -2
|
@@ -26,8 +26,8 @@ export interface HookEntry {
|
|
|
26
26
|
export interface HookGroup {
|
|
27
27
|
matcher?: string;
|
|
28
28
|
hooks: HookEntry[];
|
|
29
|
-
/** Where this group was loaded from. Project
|
|
30
|
-
_source?: "user" | "project";
|
|
29
|
+
/** Where this group was loaded from. Project AND local groups are gated on workspace trust. */
|
|
30
|
+
_source?: "user" | "project" | "local";
|
|
31
31
|
}
|
|
32
32
|
/** The hooks section of config.json. */
|
|
33
33
|
export type HooksConfig = Record<string, HookGroup[]>;
|
|
@@ -55,7 +55,7 @@ export interface HookRunner {
|
|
|
55
55
|
preToolUse(toolName: string, input: Record<string, unknown>, cwd: string, trusted?: boolean): Promise<PreToolUseHookResult>;
|
|
56
56
|
permissionRequest(toolName: string, input: Record<string, unknown>, cwd: string, trusted?: boolean): Promise<PermissionRequestHookResult>;
|
|
57
57
|
}
|
|
58
|
-
/** Read and merge hooks config from user and
|
|
58
|
+
/** Read and merge hooks config from user, project, and local files. Pure I/O, fail-soft. */
|
|
59
59
|
export declare function loadHooksConfig(userHome?: string, cwd?: string, env?: NodeJS.ProcessEnv): HooksConfig;
|
|
60
60
|
/**
|
|
61
61
|
* Check if a matcher matches a tool name. Matches Claude Code / Codex:
|
package/dist/extension/hooks.js
CHANGED
|
@@ -36,7 +36,7 @@ const SUPPORTED_EVENTS = [
|
|
|
36
36
|
// ---------------------------------------------------------------------------
|
|
37
37
|
// Config loading
|
|
38
38
|
// ---------------------------------------------------------------------------
|
|
39
|
-
/** Read and merge hooks config from user and
|
|
39
|
+
/** Read and merge hooks config from user, project, and local files. Pure I/O, fail-soft. */
|
|
40
40
|
export function loadHooksConfig(userHome = homedir(), cwd = process.cwd(), env = process.env) {
|
|
41
41
|
if (env.YAGNI_CODE_EVAL_MODE === "1")
|
|
42
42
|
return {};
|
|
@@ -47,6 +47,13 @@ export function loadHooksConfig(userHome = homedir(), cwd = process.cwd(), env =
|
|
|
47
47
|
// Project-level: .yagni-code/config.json — tagged for trust gating at execution time
|
|
48
48
|
const projectPath = join(cwd, ".yagni-code", "config.json");
|
|
49
49
|
mergeHooksFromFile(merged, projectPath, "project");
|
|
50
|
+
// Local-level: .yagni-code/config.local.json — the personal per-project
|
|
51
|
+
// tier. Trust-gated like project (NOT always-trusted): a hostile repo can
|
|
52
|
+
// COMMIT a local file — the gitignore convention only covers untracked
|
|
53
|
+
// files. Deliberately stricter than Claude Code, which does not
|
|
54
|
+
// trust-gate its localSettings hooks.
|
|
55
|
+
const localPath = join(cwd, ".yagni-code", "config.local.json");
|
|
56
|
+
mergeHooksFromFile(merged, localPath, "local");
|
|
50
57
|
return merged;
|
|
51
58
|
}
|
|
52
59
|
function mergeHooksFromFile(merged, path, source) {
|
|
@@ -68,8 +75,20 @@ function mergeHooksFromFile(merged, path, source) {
|
|
|
68
75
|
}
|
|
69
76
|
}
|
|
70
77
|
}
|
|
71
|
-
catch {
|
|
72
|
-
// Fail-soft: missing or malformed config is
|
|
78
|
+
catch (err) {
|
|
79
|
+
// Fail-soft: missing or malformed config is skipped — and LOGGED: a
|
|
80
|
+
// corrupt local/project config silently dropping its hooks (including
|
|
81
|
+
// deny-enforcing ones) with zero signal is undiagnosable. Path + error
|
|
82
|
+
// class only, matching loadConfig.ts's warning behavior.
|
|
83
|
+
logEvent({
|
|
84
|
+
source: "hooks",
|
|
85
|
+
level: "warn",
|
|
86
|
+
event: "config_warning",
|
|
87
|
+
fields: {
|
|
88
|
+
path,
|
|
89
|
+
error: err instanceof Error ? err.constructor.name : typeof err,
|
|
90
|
+
},
|
|
91
|
+
});
|
|
73
92
|
}
|
|
74
93
|
}
|
|
75
94
|
function isValidHookGroup(value) {
|
|
@@ -297,11 +316,17 @@ function logHookEvent(env, payload) {
|
|
|
297
316
|
fields,
|
|
298
317
|
});
|
|
299
318
|
}
|
|
300
|
-
/**
|
|
319
|
+
/**
|
|
320
|
+
* Filter hook groups by workspace trust: project AND local groups are
|
|
321
|
+
* skipped when untrusted. Local is gated exactly like project (not like the
|
|
322
|
+
* always-trusted user tier) because a hostile repo can COMMIT a
|
|
323
|
+
* config.local.json — the global-gitignore convention only covers
|
|
324
|
+
* untracked files. Deliberately stricter than Claude Code's localSettings.
|
|
325
|
+
*/
|
|
301
326
|
function filterByTrust(groups, isTrusted) {
|
|
302
327
|
if (isTrusted)
|
|
303
328
|
return groups;
|
|
304
|
-
return groups.filter((g) => g._source !== "project");
|
|
329
|
+
return groups.filter((g) => g._source !== "project" && g._source !== "local");
|
|
305
330
|
}
|
|
306
331
|
/**
|
|
307
332
|
* Create a HookRunner for injection into the permission gate. The `isTrusted`
|
package/dist/extension/index.js
CHANGED
|
@@ -1,6 +1,4 @@
|
|
|
1
1
|
import { createHash } from "node:crypto";
|
|
2
|
-
import { existsSync, readFileSync, renameSync, writeFileSync } from "node:fs";
|
|
3
|
-
import { join } from "node:path";
|
|
4
2
|
import { Text } from "@earendil-works/pi-tui";
|
|
5
3
|
import { DEFAULT_ADVISOR_LIMITS, formatAdvisorSubtotal, makeAdvisorState } from "./advisor.js";
|
|
6
4
|
import { makeChildUsageState } from "./childUsage.js";
|
|
@@ -26,6 +24,7 @@ import { logEvent } from "./errorSink.js";
|
|
|
26
24
|
import { registerFeedbackCommands } from "./feedbackCommand.js";
|
|
27
25
|
import { droppedSessionRuns, sessionRunIds } from "./sessionRuns.js";
|
|
28
26
|
import { codeStateHome } from "./stateHome.js";
|
|
27
|
+
import { mutateLocalConfig } from "./settingsFiles.js";
|
|
29
28
|
import { logTurnLifecycle } from "./turnLog.js";
|
|
30
29
|
import { createYagniFooterFactory, cyclePermissionMode, formatCwd, GIT_MUTATING_PATTERN, isShiftTab } from "./footer.js";
|
|
31
30
|
import { RerouteNotifier } from "./rerouteNotice.js";
|
|
@@ -607,45 +606,41 @@ export async function registerYagni(pi, deps = {}) {
|
|
|
607
606
|
if (!evalMode)
|
|
608
607
|
appendGrant(grant);
|
|
609
608
|
},
|
|
610
|
-
// persist a
|
|
611
|
-
// option
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
609
|
+
// persist a project-local allow rule (Guardian ask dialog's third
|
|
610
|
+
// option — Claude Code's own default save destination for personal
|
|
611
|
+
// rules is localSettings; ours matches). Atomic write, never overwrites
|
|
612
|
+
// other keys. THROWS on failure — the gate's catch owns the fail-soft
|
|
613
|
+
// behavior (notify the user, record ask_approved not remembered); a
|
|
614
|
+
// catch HERE would swallow the throw and make that path unreachable,
|
|
615
|
+
// leaving the user believing a rule saved that will not survive restart.
|
|
616
|
+
// Local writes fire-and-forget the global gitignore helper so the file
|
|
617
|
+
// is never committed by accident.
|
|
618
|
+
//
|
|
619
|
+
// The cwd is the GATE's per-call session cwd (a session can switch
|
|
620
|
+
// directories; process.cwd() would silently land the rule in another
|
|
621
|
+
// project's local settings while the dialog claims this one). Not
|
|
622
|
+
// registered in eval mode: the gate hides the save option when the dep
|
|
623
|
+
// is absent, so a no-op implementation that would still emit
|
|
624
|
+
// ask_approved_remembered never exists.
|
|
625
|
+
...(evalMode ? {} : {
|
|
626
|
+
persistUserRule: (ruleString, cwd) => {
|
|
627
|
+
mutateLocalConfig(cwd, (config) => {
|
|
628
|
+
const perms = (config.permissions ?? {});
|
|
629
|
+
const allow = Array.isArray(perms.allow) ? [...perms.allow] : [];
|
|
630
|
+
if (!allow.includes(ruleString))
|
|
631
|
+
allow.push(ruleString);
|
|
632
|
+
perms.allow = allow;
|
|
633
|
+
config.permissions = perms;
|
|
634
|
+
});
|
|
632
635
|
logEvent({
|
|
633
636
|
source: "permission-rules",
|
|
634
637
|
level: "info",
|
|
635
|
-
event: "
|
|
638
|
+
event: "rule_saved",
|
|
636
639
|
sessionId: env.YAGNI_SESSION_ID ?? undefined,
|
|
637
|
-
fields: { rule: ruleString },
|
|
638
|
-
});
|
|
639
|
-
}
|
|
640
|
-
catch (err) {
|
|
641
|
-
logEvent({
|
|
642
|
-
source: "permission-rules",
|
|
643
|
-
level: "warn",
|
|
644
|
-
event: "user_rule_save_failed",
|
|
645
|
-
fields: { message: err instanceof Error ? err.message : "unknown" },
|
|
640
|
+
fields: { rule: ruleString, destination: "local" },
|
|
646
641
|
});
|
|
647
|
-
}
|
|
648
|
-
},
|
|
642
|
+
},
|
|
643
|
+
}),
|
|
649
644
|
// Opt-in storage stream (YAG-510). Tier decides what leaves the machine:
|
|
650
645
|
// "off" → nothing (not even sent); "hash" → sha256 + family prefix +
|
|
651
646
|
// metadata, no command content; "raw" → adds client-REDACTED command and
|
|
@@ -231,7 +231,11 @@ export interface RegisterPermissionDeps {
|
|
|
231
231
|
* Wired by index.ts; offered as a third option on Guardian ask dialogs.
|
|
232
232
|
* Fail-soft: the in-session approval applies even if the write fails.
|
|
233
233
|
*/
|
|
234
|
-
|
|
234
|
+
/** Persist an allow rule for the caller; the cwd is the SESSION cwd of
|
|
235
|
+
* the gate call (may differ from process.cwd() when the session switches
|
|
236
|
+
* directories) — the rule must land in the project the user is working
|
|
237
|
+
* in, the same cwd the gate uses for grants and approvals. */
|
|
238
|
+
persistUserRule?: (ruleString: string, cwd: string) => void;
|
|
235
239
|
/**
|
|
236
240
|
* Called (fire-and-forget) at every terminal prompt-band outcome with the
|
|
237
241
|
* rich storage event (raw command — the wiring layer redacts/hashes).
|
|
@@ -642,7 +642,7 @@ export function registerPermissionGate(pi, deps = {}) {
|
|
|
642
642
|
}
|
|
643
643
|
catch { /* logging must never affect the gate */ }
|
|
644
644
|
if (ruleVerdict.verdict === "deny") {
|
|
645
|
-
const origin = ruleVerdict.rule.source === "
|
|
645
|
+
const origin = ruleVerdict.rule.source === "user" ? "your user settings" : ruleVerdict.rule.source === "local" ? "the project's local settings" : "the project's settings";
|
|
646
646
|
return {
|
|
647
647
|
block: true,
|
|
648
648
|
reason: `${event.toolName} was denied by a permission rule in ${origin} (${ruleVerdict.rule.raw}). Do not attempt the same outcome via a workaround or indirect execution — ask the user to change the rule if this action is genuinely needed.`,
|
|
@@ -665,7 +665,7 @@ export function registerPermissionGate(pi, deps = {}) {
|
|
|
665
665
|
const askKey = ruleAskKey(event.toolName, ruleVerdict.rule.raw, input);
|
|
666
666
|
if (ruleAskApprovals.has(askKey))
|
|
667
667
|
return {};
|
|
668
|
-
const origin = ruleVerdict.rule.source === "
|
|
668
|
+
const origin = ruleVerdict.rule.source === "user" ? "your user settings" : ruleVerdict.rule.source === "local" ? "the project's local settings" : "the project's settings";
|
|
669
669
|
const choice = await askUser(ctx, `Permission rule (ask) in ${origin}:\n${ruleVerdict.rule.raw}\nAllow ${event.toolName}?`, null);
|
|
670
670
|
if (choice === "yes") {
|
|
671
671
|
if (ruleAskApprovals.size > APPROVED_CACHE_MAX)
|
|
@@ -982,7 +982,7 @@ export function registerPermissionGate(pi, deps = {}) {
|
|
|
982
982
|
? `Yes, and don't ask again for \`${describePrefix(grantCandidate.pattern)}\` in this repo`
|
|
983
983
|
: null;
|
|
984
984
|
const ruleLabel = grantCandidate && deps.persistUserRule
|
|
985
|
-
? `Yes, and always allow \`${grantCandidate.pattern.join(" ")}\` in
|
|
985
|
+
? `Yes, and always allow \`${grantCandidate.pattern.join(" ")}\` in this project's local settings`
|
|
986
986
|
: null;
|
|
987
987
|
const resolution = await askUserWithOptions(ctx, buildAskTitle(command, verdict.rationale, verdict.riskLevel), rememberLabel, ruleLabel);
|
|
988
988
|
if (resolution === "yes") {
|
|
@@ -1000,29 +1000,35 @@ export function registerPermissionGate(pi, deps = {}) {
|
|
|
1000
1000
|
if (resolution === "rule" && ruleCandidate && deps.persistUserRule) {
|
|
1001
1001
|
let persisted = false;
|
|
1002
1002
|
try {
|
|
1003
|
-
deps.persistUserRule(ruleCandidate);
|
|
1003
|
+
deps.persistUserRule(ruleCandidate, cwd);
|
|
1004
1004
|
persisted = true;
|
|
1005
1005
|
// Also covers this session like a grant would:
|
|
1006
1006
|
rememberApproved(cwd, command);
|
|
1007
1007
|
}
|
|
1008
1008
|
catch (err) {
|
|
1009
|
-
// Fail-soft: the in-memory approval
|
|
1010
|
-
//
|
|
1011
|
-
//
|
|
1012
|
-
//
|
|
1013
|
-
//
|
|
1014
|
-
//
|
|
1009
|
+
// Fail-soft: the in-memory approval covers ONLY this call —
|
|
1010
|
+
// rememberApproved sits after the throwing call, so a failed
|
|
1011
|
+
// persist does NOT remember the command for the session (a
|
|
1012
|
+
// narrower fallback than a grant, deliberately: the user's
|
|
1013
|
+
// durable choice did not land). But the user just made an
|
|
1014
|
+
// explicit durable choice in the dialog — a silent failure
|
|
1015
|
+
// would leave them believing a rule exists that will not
|
|
1016
|
+
// survive restart. Log the failure (rule string + the thrown
|
|
1017
|
+
// message — it is mutateConfigJson's own path/reason text,
|
|
1018
|
+
// never the raw parse error or file content) and tell the
|
|
1019
|
+
// user.
|
|
1015
1020
|
logEvent({
|
|
1016
1021
|
source: "permission-rules",
|
|
1017
1022
|
level: "warn",
|
|
1018
|
-
event: "
|
|
1023
|
+
event: "rule_save_failed",
|
|
1019
1024
|
fields: {
|
|
1020
1025
|
rule: ruleCandidate,
|
|
1021
|
-
|
|
1026
|
+
destination: "local",
|
|
1027
|
+
error: err instanceof Error ? err.message : String(err),
|
|
1022
1028
|
},
|
|
1023
1029
|
});
|
|
1024
1030
|
if (ctx?.hasUI) {
|
|
1025
|
-
ctx.ui.notify(`Could not save the permission rule to your settings — it
|
|
1031
|
+
ctx.ui.notify(`Could not save the permission rule to your settings — it did not persist; you'll be asked again on the next identical command.`, "warning");
|
|
1026
1032
|
}
|
|
1027
1033
|
}
|
|
1028
1034
|
emitGateEvent(slot, {
|
|
@@ -1,23 +1,29 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Permission-rule settings loader.
|
|
3
3
|
*
|
|
4
|
-
* Reads `permissions: { allow, deny, ask }` from the
|
|
5
|
-
* files, mirroring the hooks loader (hooks.ts) shape exactly:
|
|
6
|
-
* - user:
|
|
7
|
-
* - project:
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
4
|
+
* Reads `permissions: { allow, deny, ask }` from the three YAGNI Code
|
|
5
|
+
* settings files, mirroring the hooks loader (hooks.ts) shape exactly:
|
|
6
|
+
* - user: ~/.yagni-code/config.json — always active
|
|
7
|
+
* - project: .yagni-code/config.json — deny/ask always active;
|
|
8
|
+
* allow gated on workspace trust (Claude Code semantics:
|
|
9
|
+
* "deny and ask rules apply right away; allow rules from a
|
|
10
|
+
* project file wait for trust")
|
|
11
|
+
* - local: .yagni-code/config.local.json — the personal per-project
|
|
12
|
+
* tier; trust-gated EXACTLY like project (a hostile repo can
|
|
13
|
+
* commit a local file — the gitignore convention only covers
|
|
14
|
+
* untracked files). Deliberately stricter than Claude Code,
|
|
15
|
+
* which does not trust-gate localSettings.
|
|
11
16
|
*
|
|
12
17
|
* Lists UNION across sources (Claude Code: lists merge, never replace).
|
|
13
|
-
* User-source rules are ordered before project-source rules
|
|
14
|
-
* negation (`!exception`) behaves like gitignore
|
|
18
|
+
* User-source rules are ordered before project-source rules, project before
|
|
19
|
+
* local, so path-pattern negation (`!exception`) behaves like gitignore
|
|
20
|
+
* lines across files — higher-precedence sources come later (later wins).
|
|
15
21
|
*
|
|
16
22
|
* Fail-soft: a malformed config file is skipped whole with a warning (same
|
|
17
23
|
* posture as hooks.ts). Malformed individual rule strings parse as bare
|
|
18
24
|
* tool names (parser degrades, never throws) and are flagged.
|
|
19
25
|
*/
|
|
20
|
-
export type RuleSource = "user" | "project";
|
|
26
|
+
export type RuleSource = "user" | "project" | "local";
|
|
21
27
|
export type RuleBehavior = "allow" | "deny" | "ask";
|
|
22
28
|
/** A parsed, sourced permission rule. */
|
|
23
29
|
export interface PermissionRule {
|
|
@@ -40,7 +46,7 @@ export interface LoadedRules {
|
|
|
40
46
|
}
|
|
41
47
|
/** The tools a rule can actually be consulted for at the gate. */
|
|
42
48
|
export declare const KNOWN_RULE_TOOLS: Set<string>;
|
|
43
|
-
/** Load + union rules from
|
|
49
|
+
/** Load + union rules from all three files. Pure I/O; no throw. */
|
|
44
50
|
export declare function loadPermissionRules(opts?: {
|
|
45
51
|
cwd?: string;
|
|
46
52
|
env?: NodeJS.ProcessEnv;
|
|
@@ -48,6 +54,11 @@ export declare function loadPermissionRules(opts?: {
|
|
|
48
54
|
/** Override the user state home (tests). */
|
|
49
55
|
stateHomeOverride?: string | null;
|
|
50
56
|
}): LoadedRules;
|
|
51
|
-
/**
|
|
57
|
+
/**
|
|
58
|
+
* Filter rules by behavior + trust. Untrusted repos keep deny/ask from every
|
|
59
|
+
* source but lose allow rules from BOTH project and local files — a
|
|
60
|
+
* committed config.local.json must not smuggle allow rules past the trust
|
|
61
|
+
* gate (see the module header; stricter than Claude Code's localSettings).
|
|
62
|
+
*/
|
|
52
63
|
export declare function effectiveRules(rules: readonly PermissionRule[], isProjectTrusted: boolean): PermissionRule[];
|
|
53
64
|
//# sourceMappingURL=loadConfig.d.ts.map
|
|
@@ -1,17 +1,23 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Permission-rule settings loader.
|
|
3
3
|
*
|
|
4
|
-
* Reads `permissions: { allow, deny, ask }` from the
|
|
5
|
-
* files, mirroring the hooks loader (hooks.ts) shape exactly:
|
|
6
|
-
* - user:
|
|
7
|
-
* - project:
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
4
|
+
* Reads `permissions: { allow, deny, ask }` from the three YAGNI Code
|
|
5
|
+
* settings files, mirroring the hooks loader (hooks.ts) shape exactly:
|
|
6
|
+
* - user: ~/.yagni-code/config.json — always active
|
|
7
|
+
* - project: .yagni-code/config.json — deny/ask always active;
|
|
8
|
+
* allow gated on workspace trust (Claude Code semantics:
|
|
9
|
+
* "deny and ask rules apply right away; allow rules from a
|
|
10
|
+
* project file wait for trust")
|
|
11
|
+
* - local: .yagni-code/config.local.json — the personal per-project
|
|
12
|
+
* tier; trust-gated EXACTLY like project (a hostile repo can
|
|
13
|
+
* commit a local file — the gitignore convention only covers
|
|
14
|
+
* untracked files). Deliberately stricter than Claude Code,
|
|
15
|
+
* which does not trust-gate localSettings.
|
|
11
16
|
*
|
|
12
17
|
* Lists UNION across sources (Claude Code: lists merge, never replace).
|
|
13
|
-
* User-source rules are ordered before project-source rules
|
|
14
|
-
* negation (`!exception`) behaves like gitignore
|
|
18
|
+
* User-source rules are ordered before project-source rules, project before
|
|
19
|
+
* local, so path-pattern negation (`!exception`) behaves like gitignore
|
|
20
|
+
* lines across files — higher-precedence sources come later (later wins).
|
|
15
21
|
*
|
|
16
22
|
* Fail-soft: a malformed config file is skipped whole with a warning (same
|
|
17
23
|
* posture as hooks.ts). Malformed individual rule strings parse as bare
|
|
@@ -64,27 +70,36 @@ function readRulesFromFile(path, source, behavior, out, warnings) {
|
|
|
64
70
|
warnings.push(`${source} settings: malformed JSON — permissions skipped (${path})`);
|
|
65
71
|
}
|
|
66
72
|
}
|
|
67
|
-
/** Load + union rules from
|
|
73
|
+
/** Load + union rules from all three files. Pure I/O; no throw. */
|
|
68
74
|
export function loadPermissionRules(opts = {}) {
|
|
69
75
|
const warnings = [];
|
|
70
76
|
const rules = [];
|
|
71
77
|
const stateHome = opts.stateHomeOverride ?? codeStateHome(null, opts.env, opts.userHome);
|
|
78
|
+
const cwd = opts.cwd ?? process.cwd();
|
|
72
79
|
const userPath = join(stateHome, "config.json");
|
|
73
|
-
const projectPath = join(
|
|
74
|
-
|
|
80
|
+
const projectPath = join(cwd, ".yagni-code", "config.json");
|
|
81
|
+
const localPath = join(cwd, ".yagni-code", "config.local.json");
|
|
82
|
+
// User, then project, then local — ordering matters for path-rule
|
|
83
|
+
// negation (later sources win, matching scalar precedence).
|
|
75
84
|
for (const behavior of ["deny", "ask", "allow"]) {
|
|
76
85
|
readRulesFromFile(userPath, "user", behavior, rules, warnings);
|
|
77
86
|
readRulesFromFile(projectPath, "project", behavior, rules, warnings);
|
|
87
|
+
readRulesFromFile(localPath, "local", behavior, rules, warnings);
|
|
78
88
|
}
|
|
79
89
|
const neverConsultedTools = [
|
|
80
90
|
...new Set(rules.map((r) => r.toolName).filter((t) => !KNOWN_RULE_TOOLS.has(t) && !t.startsWith("mcp__"))),
|
|
81
91
|
];
|
|
82
92
|
return { rules, diagnostics: { warnings, neverConsultedTools } };
|
|
83
93
|
}
|
|
84
|
-
/**
|
|
94
|
+
/**
|
|
95
|
+
* Filter rules by behavior + trust. Untrusted repos keep deny/ask from every
|
|
96
|
+
* source but lose allow rules from BOTH project and local files — a
|
|
97
|
+
* committed config.local.json must not smuggle allow rules past the trust
|
|
98
|
+
* gate (see the module header; stricter than Claude Code's localSettings).
|
|
99
|
+
*/
|
|
85
100
|
export function effectiveRules(rules, isProjectTrusted) {
|
|
86
101
|
if (isProjectTrusted)
|
|
87
102
|
return [...rules];
|
|
88
|
-
return rules.filter((r) => !(r.source
|
|
103
|
+
return rules.filter((r) => !(r.source !== "user" && r.behavior === "allow"));
|
|
89
104
|
}
|
|
90
105
|
//# sourceMappingURL=loadConfig.js.map
|
|
@@ -8,15 +8,17 @@
|
|
|
8
8
|
* //path absolute from filesystem root
|
|
9
9
|
* ~/path relative to the user's home directory
|
|
10
10
|
* /path relative to the SETTINGS FILE's directory (user config →
|
|
11
|
-
* ~/.yagni-code/, project
|
|
11
|
+
* ~/.yagni-code/, project AND local configs → the project root —
|
|
12
|
+
* local lives in the same .yagni-code dir as project)
|
|
12
13
|
* path relative to the session cwd; a bare filename matches at ANY
|
|
13
14
|
* depth (`.env` equals `**` / `.env` in glob terms)
|
|
14
15
|
*
|
|
15
16
|
* Compile semantics: all patterns for one (tool-class, behavior) compile
|
|
16
17
|
* into ONE ordered `ignore` instance — user-source patterns first, then
|
|
17
|
-
* project — so gitignore's later-line-wins and `!exception`
|
|
18
|
-
* across rules from
|
|
19
|
-
* one ignore().add(patterns) per root the same
|
|
18
|
+
* project, then local — so gitignore's later-line-wins and `!exception`
|
|
19
|
+
* negation work across rules from all three files (Claude Code's
|
|
20
|
+
* matchingRuleForInput builds one ignore().add(patterns) per root the same
|
|
21
|
+
* way; local patterns are the last/highest-precedence "lines").
|
|
20
22
|
*
|
|
21
23
|
* Behavior asymmetry (Claude Code parity): allow rules with a rootless
|
|
22
24
|
* single-segment pattern anchor at the settings dir (narrow); deny/ask
|
|
@@ -40,9 +42,9 @@ export interface ResolvedPathPattern {
|
|
|
40
42
|
}
|
|
41
43
|
/**
|
|
42
44
|
* Split an anchored pattern into (root, relativePattern). Mirrors Claude
|
|
43
|
-
* Code's patternWithRoot. Project-source `/` anchors at the
|
|
44
|
-
* (passed in)
|
|
45
|
-
* (~/.yagni-code).
|
|
45
|
+
* Code's patternWithRoot. Project- and local-source `/` anchors at the
|
|
46
|
+
* project root (passed in) — local shares project's .yagni-code dir;
|
|
47
|
+
* user-source `/` anchors at the YAGNI Code state home (~/.yagni-code).
|
|
46
48
|
*/
|
|
47
49
|
export declare function resolvePatternRoot(pattern: string, source: RuleSource, opts: {
|
|
48
50
|
userStateHome: string;
|
|
@@ -8,15 +8,17 @@
|
|
|
8
8
|
* //path absolute from filesystem root
|
|
9
9
|
* ~/path relative to the user's home directory
|
|
10
10
|
* /path relative to the SETTINGS FILE's directory (user config →
|
|
11
|
-
* ~/.yagni-code/, project
|
|
11
|
+
* ~/.yagni-code/, project AND local configs → the project root —
|
|
12
|
+
* local lives in the same .yagni-code dir as project)
|
|
12
13
|
* path relative to the session cwd; a bare filename matches at ANY
|
|
13
14
|
* depth (`.env` equals `**` / `.env` in glob terms)
|
|
14
15
|
*
|
|
15
16
|
* Compile semantics: all patterns for one (tool-class, behavior) compile
|
|
16
17
|
* into ONE ordered `ignore` instance — user-source patterns first, then
|
|
17
|
-
* project — so gitignore's later-line-wins and `!exception`
|
|
18
|
-
* across rules from
|
|
19
|
-
* one ignore().add(patterns) per root the same
|
|
18
|
+
* project, then local — so gitignore's later-line-wins and `!exception`
|
|
19
|
+
* negation work across rules from all three files (Claude Code's
|
|
20
|
+
* matchingRuleForInput builds one ignore().add(patterns) per root the same
|
|
21
|
+
* way; local patterns are the last/highest-precedence "lines").
|
|
20
22
|
*
|
|
21
23
|
* Behavior asymmetry (Claude Code parity): allow rules with a rootless
|
|
22
24
|
* single-segment pattern anchor at the settings dir (narrow); deny/ask
|
|
@@ -42,9 +44,9 @@ export function pathClassForTool(toolName) {
|
|
|
42
44
|
}
|
|
43
45
|
/**
|
|
44
46
|
* Split an anchored pattern into (root, relativePattern). Mirrors Claude
|
|
45
|
-
* Code's patternWithRoot. Project-source `/` anchors at the
|
|
46
|
-
* (passed in)
|
|
47
|
-
* (~/.yagni-code).
|
|
47
|
+
* Code's patternWithRoot. Project- and local-source `/` anchors at the
|
|
48
|
+
* project root (passed in) — local shares project's .yagni-code dir;
|
|
49
|
+
* user-source `/` anchors at the YAGNI Code state home (~/.yagni-code).
|
|
48
50
|
*/
|
|
49
51
|
export function resolvePatternRoot(pattern, source, opts) {
|
|
50
52
|
if (pattern.startsWith("//")) {
|
|
@@ -55,7 +57,7 @@ export function resolvePatternRoot(pattern, source, opts) {
|
|
|
55
57
|
return { relativePattern: rel, root: opts.homeDir ?? homedir(), source };
|
|
56
58
|
}
|
|
57
59
|
if (pattern.startsWith("/") && !pattern.startsWith("//")) {
|
|
58
|
-
const base = source === "
|
|
60
|
+
const base = source === "user" ? opts.userStateHome : opts.projectRoot;
|
|
59
61
|
return { relativePattern: pattern, root: base ?? opts.userStateHome, source };
|
|
60
62
|
}
|
|
61
63
|
// Rootless: `./x` normalized to `x`; bare names keep any-depth semantics.
|
|
@@ -1,16 +1,17 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Sandbox settings schema + layered config load (user + project),
|
|
3
|
-
* Claude Code's SandboxSettingsSchema subset so a Claude
|
|
4
|
-
* copied verbatim parses and resolves identically.
|
|
2
|
+
* Sandbox settings schema + layered config load (user + project + local),
|
|
3
|
+
* shaped after Claude Code's SandboxSettingsSchema subset so a Claude
|
|
4
|
+
* `sandbox` block copied verbatim parses and resolves identically.
|
|
5
5
|
*
|
|
6
|
-
* Files: the `sandbox` key in ~/.yagni-code/config.json (user)
|
|
7
|
-
* .yagni-code/config.json (project)
|
|
8
|
-
*
|
|
6
|
+
* Files: the `sandbox` key in ~/.yagni-code/config.json (user),
|
|
7
|
+
* .yagni-code/config.json (project), and .yagni-code/config.local.json
|
|
8
|
+
* (local — the personal per-project tier; the /sandbox panel and toggle
|
|
9
|
+
* persist HERE).
|
|
9
10
|
*
|
|
10
|
-
* Merge semantics (Claude parity): scalars —
|
|
11
|
-
* defaults; arrays — union + dedupe across sources (lists merge,
|
|
12
|
-
* replace). Fail-soft: a malformed file is skipped whole with a
|
|
13
|
-
* (same posture as permissionRules/loadConfig.ts).
|
|
11
|
+
* Merge semantics (Claude parity): scalars — local wins over project over
|
|
12
|
+
* user over defaults; arrays — union + dedupe across sources (lists merge,
|
|
13
|
+
* never replace). Fail-soft: a malformed file is skipped whole with a
|
|
14
|
+
* warning (same posture as permissionRules/loadConfig.ts).
|
|
14
15
|
*
|
|
15
16
|
* Read/write filesystem semantics (srt, verified in the M0 spike): reads are
|
|
16
17
|
* allowed by default with denyRead regions + allowRead re-allow; writes are
|
|
@@ -64,10 +65,10 @@ export interface LoadedSandboxConfig {
|
|
|
64
65
|
*/
|
|
65
66
|
export declare function readSandboxSettingsFromFile(configPath: string, warnings: string[], unknownKeys: string[]): SandboxSettings | undefined;
|
|
66
67
|
/**
|
|
67
|
-
* Load + merge sandbox settings from
|
|
68
|
-
* beats user; arrays: union. Defaults for scalars land here
|
|
69
|
-
* parity): autoAllowBashIfSandboxed true, allowUnsandboxedCommands
|
|
70
|
-
* everything else unset/false.
|
|
68
|
+
* Load + merge sandbox settings from all three config files. Scalars: local
|
|
69
|
+
* beats project beats user; arrays: union. Defaults for scalars land here
|
|
70
|
+
* too (Claude parity): autoAllowBashIfSandboxed true, allowUnsandboxedCommands
|
|
71
|
+
* true, everything else unset/false.
|
|
71
72
|
*/
|
|
72
73
|
export declare function loadSandboxSettings(opts?: {
|
|
73
74
|
cwd?: string;
|