@deftai/directive-core 0.86.0 → 0.88.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/cache/scanner.d.ts +11 -1
- package/dist/cache/scanner.js +29 -4
- package/dist/check/gate-lists.js +2 -0
- package/dist/content-contracts/skills/helpers.d.ts +10 -0
- package/dist/content-contracts/skills/helpers.js +35 -0
- package/dist/deposit/copy-tree.d.ts +19 -1
- package/dist/deposit/copy-tree.js +134 -5
- package/dist/doctor/main.d.ts +6 -5
- package/dist/doctor/main.js +80 -18
- package/dist/doctor/taskfile.d.ts +8 -0
- package/dist/doctor/taskfile.js +19 -0
- package/dist/fs/projection-containment.d.ts +18 -0
- package/dist/fs/projection-containment.js +40 -0
- package/dist/hooks/dispatcher.d.ts +34 -2
- package/dist/hooks/dispatcher.js +234 -21
- package/dist/hooks/tools.d.ts +31 -0
- package/dist/hooks/tools.js +74 -0
- package/dist/init-deposit/agent-hooks.d.ts +1 -1
- package/dist/init-deposit/agent-hooks.js +38 -2
- package/dist/init-deposit/hygiene.d.ts +16 -0
- package/dist/init-deposit/hygiene.js +26 -0
- package/dist/init-deposit/init-dispatch.js +28 -0
- package/dist/init-deposit/prettierignore.js +2 -2
- package/dist/init-deposit/refresh.js +38 -7
- package/dist/init-deposit/scaffold.js +7 -3
- package/dist/init-deposit/xbrief-projections.js +6 -6
- package/dist/intake/issue-emit.d.ts +45 -2
- package/dist/intake/issue-emit.js +420 -17
- package/dist/intake/issue-ingest.js +65 -6
- package/dist/packs/pack-render.d.ts +33 -0
- package/dist/packs/pack-render.js +155 -9
- package/dist/packs/quarantine-ext.d.ts +10 -0
- package/dist/packs/quarantine-ext.js +26 -2
- package/dist/platform/platform-capabilities.js +3 -0
- package/dist/policy/index.d.ts +1 -0
- package/dist/policy/index.js +1 -0
- package/dist/policy/no-deft-directive.d.ts +59 -0
- package/dist/policy/no-deft-directive.js +103 -0
- package/dist/policy/org-force-on-migration.d.ts +52 -0
- package/dist/policy/org-force-on-migration.js +260 -22
- package/dist/policy/runtime-authority.d.ts +41 -0
- package/dist/policy/runtime-authority.js +274 -0
- package/dist/review-monitor/constants.js +3 -2
- package/dist/review-monitor/tier-detection.d.ts +6 -2
- package/dist/review-monitor/tier-detection.js +27 -2
- package/dist/scope/transition.js +43 -0
- package/dist/session/release-availability.d.ts +2 -0
- package/dist/session/release-availability.js +23 -8
- package/dist/session/session-start-hook.d.ts +3 -0
- package/dist/session/session-start-hook.js +15 -0
- package/dist/session/session-start.js +30 -0
- package/dist/swarm/routing-set-cli.js +5 -10
- package/dist/swarm/routing.d.ts +3 -2
- package/dist/swarm/routing.js +16 -4
- package/dist/triage/help/registry-data.d.ts +7 -7
- package/dist/triage/help/registry-data.js +15 -6
- package/dist/triage/queue/index.d.ts +1 -0
- package/dist/triage/queue/index.js +1 -0
- package/dist/triage/queue/show.d.ts +69 -0
- package/dist/triage/queue/show.js +293 -0
- package/dist/triage/scope/cli.js +3 -0
- package/dist/triage/scope/coverage.d.ts +2 -0
- package/dist/triage/scope/coverage.js +18 -3
- package/dist/verify-source/cursor-tier1.js +7 -2
- package/dist/verify-source/index.d.ts +1 -0
- package/dist/verify-source/index.js +1 -0
- package/dist/verify-source/openclaw-tier1.d.ts +37 -0
- package/dist/verify-source/openclaw-tier1.js +105 -0
- package/dist/xbrief-migrate/migrate-project.js +9 -5
- package/package.json +4 -3
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/** Port of scripts/pack_render.py — content-pack projection renderer (#1294, #1295). */
|
|
2
|
-
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
3
|
-
import { dirname, join, resolve } from "node:path";
|
|
2
|
+
import { existsSync, lstatSync, mkdirSync, readFileSync, realpathSync, writeFileSync, } from "node:fs";
|
|
3
|
+
import { dirname, isAbsolute, join, relative, resolve } from "node:path";
|
|
4
4
|
import { fileURLToPath } from "node:url";
|
|
5
5
|
import { contentRoot } from "../content-root.js";
|
|
6
6
|
export function resolveRepoRoot() {
|
|
@@ -54,6 +54,138 @@ const SWARM_SPEC_BANNER = [
|
|
|
54
54
|
];
|
|
55
55
|
export const BANNER = `${LESSONS_BANNER.join("\n")}\n`;
|
|
56
56
|
export const DOC_TITLE = "# Lessons Learned";
|
|
57
|
+
/** Non-zero exit code for a pack-render containment / schema refusal. */
|
|
58
|
+
export const PACK_RENDER_REFUSED_EXIT_CODE = 2;
|
|
59
|
+
/**
|
|
60
|
+
* Thrown, fail-closed, when a pack record's projection path escapes CONTENT_ROOT,
|
|
61
|
+
* violates the pack path schema, or a frontmatter field carries an injection
|
|
62
|
+
* payload (null byte / newline). Remediates AppSec finding skill-pack-supply-01
|
|
63
|
+
* from tracker #2904. Refs #2914.
|
|
64
|
+
*/
|
|
65
|
+
export class PackRenderContainmentError extends Error {
|
|
66
|
+
pack;
|
|
67
|
+
field;
|
|
68
|
+
offendingValue;
|
|
69
|
+
constructor(message, details) {
|
|
70
|
+
super(message);
|
|
71
|
+
this.name = "PackRenderContainmentError";
|
|
72
|
+
this.pack = details.pack;
|
|
73
|
+
this.field = details.field;
|
|
74
|
+
this.offendingValue = details.offendingValue;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* Path-SEGMENT containment: is `child` equal to `parent` or nested under it?
|
|
79
|
+
* Uses `path.relative` so `/foo` is NOT treated as containing `/foobar`
|
|
80
|
+
* (mirrors deposit/contain.ts).
|
|
81
|
+
*/
|
|
82
|
+
function isContained(parent, child) {
|
|
83
|
+
if (parent === child) {
|
|
84
|
+
return true;
|
|
85
|
+
}
|
|
86
|
+
const rel = relative(parent, child);
|
|
87
|
+
return rel.length > 0 && !rel.startsWith("..") && !isAbsolute(rel);
|
|
88
|
+
}
|
|
89
|
+
/** realpath with a resolve() fallback when the path does not exist yet. */
|
|
90
|
+
function safeRealpath(p) {
|
|
91
|
+
try {
|
|
92
|
+
return realpathSync(p);
|
|
93
|
+
}
|
|
94
|
+
catch {
|
|
95
|
+
return resolve(p);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
/**
|
|
99
|
+
* Defense-in-depth: walk each EXISTING component from CONTENT_ROOT down to the
|
|
100
|
+
* resolved output and refuse any symlink that escapes CONTENT_ROOT. Mirrors the
|
|
101
|
+
* realpath-anchored segment walk in deposit/contain.ts so a symlinked content
|
|
102
|
+
* subtree cannot redirect a projection write outside the shippable-content root.
|
|
103
|
+
*/
|
|
104
|
+
function assertAncestorContained(packName, field, rawPath, rootReal, outPath) {
|
|
105
|
+
const rel = relative(rootReal, outPath);
|
|
106
|
+
const segments = rel.split(/[\\/]+/).filter((segment) => segment.length > 0);
|
|
107
|
+
let current = rootReal;
|
|
108
|
+
for (const segment of segments) {
|
|
109
|
+
current = join(current, segment);
|
|
110
|
+
let info;
|
|
111
|
+
try {
|
|
112
|
+
info = lstatSync(current);
|
|
113
|
+
}
|
|
114
|
+
catch {
|
|
115
|
+
// Component does not exist yet; nothing below it can exist either.
|
|
116
|
+
break;
|
|
117
|
+
}
|
|
118
|
+
if (info.isSymbolicLink()) {
|
|
119
|
+
let linkReal;
|
|
120
|
+
try {
|
|
121
|
+
linkReal = realpathSync(current);
|
|
122
|
+
}
|
|
123
|
+
catch {
|
|
124
|
+
throw new PackRenderContainmentError(`pack-render refused: ${packName} ${field} path component ${current} is a broken symlink`, { pack: packName, field, offendingValue: rawPath });
|
|
125
|
+
}
|
|
126
|
+
if (!isContained(rootReal, linkReal)) {
|
|
127
|
+
throw new PackRenderContainmentError(`pack-render refused: ${packName} ${field} path component ${current} is a symlink ` +
|
|
128
|
+
`escaping CONTENT_ROOT (resolves to ${linkReal}, outside ${rootReal})`, { pack: packName, field, offendingValue: rawPath });
|
|
129
|
+
}
|
|
130
|
+
current = linkReal;
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
/**
|
|
135
|
+
* Resolve a pack record's projection path against CONTENT_ROOT and refuse,
|
|
136
|
+
* fail-closed, any value that: is missing/empty/non-string, carries a null
|
|
137
|
+
* byte, is absolute, violates the pack's schema path pattern, or resolves
|
|
138
|
+
* outside CONTENT_ROOT (a `..` escape). Returns the contained absolute output
|
|
139
|
+
* path. Mirrors deposit/contain.ts segment containment. Refs #2914 / #2904.
|
|
140
|
+
*/
|
|
141
|
+
export function containedOutPath(packName, rawPath, cfg) {
|
|
142
|
+
const field = cfg.path_field ?? "path";
|
|
143
|
+
if (typeof rawPath !== "string" || rawPath.length === 0) {
|
|
144
|
+
throw new PackRenderContainmentError(`pack-render refused: ${packName} entry has a missing/empty ${field}`, { pack: packName, field, offendingValue: String(rawPath) });
|
|
145
|
+
}
|
|
146
|
+
if (rawPath.includes("\0")) {
|
|
147
|
+
throw new PackRenderContainmentError(`pack-render refused: ${packName} ${field} contains a null byte`, { pack: packName, field, offendingValue: rawPath });
|
|
148
|
+
}
|
|
149
|
+
if (isAbsolute(rawPath) || /^[\\/]/.test(rawPath)) {
|
|
150
|
+
throw new PackRenderContainmentError(`pack-render refused: ${packName} ${field} must be a relative path (got absolute ${JSON.stringify(rawPath)})`, { pack: packName, field, offendingValue: rawPath });
|
|
151
|
+
}
|
|
152
|
+
if (cfg.path_pattern && !cfg.path_pattern.test(rawPath)) {
|
|
153
|
+
throw new PackRenderContainmentError(`pack-render refused: ${packName} ${field} ${JSON.stringify(rawPath)} violates the ` +
|
|
154
|
+
`${packName}-pack schema path pattern ${String(cfg.path_pattern)}`, { pack: packName, field, offendingValue: rawPath });
|
|
155
|
+
}
|
|
156
|
+
// Resolve with realpath semantics on the trusted root, then confirm segment
|
|
157
|
+
// containment. `resolve` collapses `..`, so an escape lands outside rootReal
|
|
158
|
+
// and is refused here even when it slipped past the schema pattern (where `.`
|
|
159
|
+
// can match a path separator).
|
|
160
|
+
const rootReal = safeRealpath(CONTENT_ROOT);
|
|
161
|
+
const outPath = resolve(rootReal, rawPath);
|
|
162
|
+
if (!isContained(rootReal, outPath)) {
|
|
163
|
+
throw new PackRenderContainmentError(`pack-render refused: ${packName} ${field} ${JSON.stringify(rawPath)} escapes CONTENT_ROOT ` +
|
|
164
|
+
`(resolves to ${outPath}, outside ${rootReal})`, { pack: packName, field, offendingValue: rawPath });
|
|
165
|
+
}
|
|
166
|
+
assertAncestorContained(packName, field, rawPath, rootReal, outPath);
|
|
167
|
+
return outPath;
|
|
168
|
+
}
|
|
169
|
+
/** Refuse a frontmatter field value that embeds a null byte. Refs #2914. */
|
|
170
|
+
function assertNoNullByte(value, packName, field) {
|
|
171
|
+
if (value.includes("\0")) {
|
|
172
|
+
throw new PackRenderContainmentError(`pack-render refused: ${packName} frontmatter field ${field} contains a null byte`, { pack: packName, field, offendingValue: value });
|
|
173
|
+
}
|
|
174
|
+
return value;
|
|
175
|
+
}
|
|
176
|
+
/**
|
|
177
|
+
* Sanitize a single-line YAML frontmatter field (skill `name`): refuse null
|
|
178
|
+
* bytes and any CR/LF. An unescaped newline would inject arbitrary YAML keys or
|
|
179
|
+
* a premature `---` document break into the generated frontmatter. Refs #2914.
|
|
180
|
+
*/
|
|
181
|
+
function sanitizeSingleLineField(value, packName, field) {
|
|
182
|
+
assertNoNullByte(value, packName, field);
|
|
183
|
+
if (/[\r\n]/.test(value)) {
|
|
184
|
+
throw new PackRenderContainmentError(`pack-render refused: ${packName} frontmatter field ${field} must not contain a newline ` +
|
|
185
|
+
`(YAML frontmatter injection)`, { pack: packName, field, offendingValue: value });
|
|
186
|
+
}
|
|
187
|
+
return value;
|
|
188
|
+
}
|
|
57
189
|
export const RENDER_REGISTRY = {
|
|
58
190
|
lessons: {
|
|
59
191
|
mode: "collection",
|
|
@@ -73,6 +205,7 @@ export const RENDER_REGISTRY = {
|
|
|
73
205
|
name_field: "id",
|
|
74
206
|
description_field: "description",
|
|
75
207
|
path_field: "path",
|
|
208
|
+
path_pattern: /^skills\/.+\/SKILL\.md$/,
|
|
76
209
|
body_field: "body",
|
|
77
210
|
banner: SKILLS_BANNER,
|
|
78
211
|
},
|
|
@@ -82,6 +215,7 @@ export const RENDER_REGISTRY = {
|
|
|
82
215
|
source: join(CONTENT_ROOT, "packs", "rules", "rules-pack-0.1.json"),
|
|
83
216
|
items_field: "rules",
|
|
84
217
|
path_field: "path",
|
|
218
|
+
path_pattern: /^(?:coding\/.+\.md|AGENTS\.md|main\.md)$/,
|
|
85
219
|
body_field: "body",
|
|
86
220
|
banner: RULES_BANNER,
|
|
87
221
|
},
|
|
@@ -91,6 +225,7 @@ export const RENDER_REGISTRY = {
|
|
|
91
225
|
source: join(CONTENT_ROOT, "packs", "strategies", "strategies-pack-0.1.json"),
|
|
92
226
|
items_field: "strategies",
|
|
93
227
|
path_field: "path",
|
|
228
|
+
path_pattern: /^strategies\/.+\.md$/,
|
|
94
229
|
body_field: "body",
|
|
95
230
|
banner: STRATEGIES_BANNER,
|
|
96
231
|
},
|
|
@@ -100,6 +235,7 @@ export const RENDER_REGISTRY = {
|
|
|
100
235
|
source: join(CONTENT_ROOT, "packs", "patterns", "patterns-pack-0.1.json"),
|
|
101
236
|
items_field: "patterns",
|
|
102
237
|
path_field: "path",
|
|
238
|
+
path_pattern: /^patterns\/.+\.md$/,
|
|
103
239
|
body_field: "body",
|
|
104
240
|
banner: PATTERNS_BANNER,
|
|
105
241
|
},
|
|
@@ -109,6 +245,7 @@ export const RENDER_REGISTRY = {
|
|
|
109
245
|
source: join(CONTENT_ROOT, "packs", "swarm-spec", "swarm-spec-pack-0.1.json"),
|
|
110
246
|
items_field: "entries",
|
|
111
247
|
path_field: "path",
|
|
248
|
+
path_pattern: /^swarm\/.+\.md$/,
|
|
112
249
|
body_field: "body",
|
|
113
250
|
banner: SWARM_SPEC_BANNER,
|
|
114
251
|
},
|
|
@@ -166,12 +303,18 @@ function emitDescription(description) {
|
|
|
166
303
|
return lines.join("\n");
|
|
167
304
|
}
|
|
168
305
|
export function renderSkillDocument(entry, cfg) {
|
|
169
|
-
|
|
170
|
-
|
|
306
|
+
// Sanitize every YAML frontmatter field so a malicious pack cannot inject
|
|
307
|
+
// unescaped frontmatter (newline -> extra key / premature `---`) or a null
|
|
308
|
+
// byte. Refs #2914 / #2904 (skill-pack-supply-01).
|
|
309
|
+
const nameField = cfg.name_field ?? "id";
|
|
310
|
+
const descriptionField = cfg.description_field ?? "description";
|
|
311
|
+
const name = sanitizeSingleLineField(String(entry[nameField]), "skills", nameField);
|
|
312
|
+
const description = assertNoNullByte(String(entry[descriptionField]), "skills", descriptionField);
|
|
171
313
|
const body = entry[cfg.body_field] ?? "";
|
|
172
314
|
const extra = entry.frontmatter_extra;
|
|
173
|
-
const
|
|
174
|
-
const
|
|
315
|
+
const extraStr = typeof extra === "string" ? assertNoNullByte(extra, "skills", "frontmatter_extra") : "";
|
|
316
|
+
const extraBlock = extraStr.length > 0 ? `${extraStr}\n` : "";
|
|
317
|
+
const frontmatter = `---\nname: ${name}\n${emitDescription(description)}\n${extraBlock}---\n`;
|
|
175
318
|
return `${frontmatter}${bannerText(cfg.banner)}\n${String(body)}`;
|
|
176
319
|
}
|
|
177
320
|
export function renderMarkdownDocument(entry, cfg) {
|
|
@@ -182,7 +325,7 @@ const DOCUMENT_RENDERERS = {
|
|
|
182
325
|
skill: renderSkillDocument,
|
|
183
326
|
markdown: renderMarkdownDocument,
|
|
184
327
|
};
|
|
185
|
-
function targetsForPack(pack, cfg) {
|
|
328
|
+
export function targetsForPack(packName, pack, cfg) {
|
|
186
329
|
if (cfg.mode === "collection") {
|
|
187
330
|
return [[cfg.output ?? "", renderCollection(pack, cfg)]];
|
|
188
331
|
}
|
|
@@ -203,7 +346,10 @@ function targetsForPack(pack, cfg) {
|
|
|
203
346
|
const pathField = cfg.path_field ?? "path";
|
|
204
347
|
// Document projection targets (skills/rules/strategies/patterns) are
|
|
205
348
|
// shippable content -- they live under content/ in the source repo (#1875).
|
|
206
|
-
|
|
349
|
+
// #2914: contain the pack-supplied path under CONTENT_ROOT and enforce the
|
|
350
|
+
// pack schema path pattern BEFORE render so a malicious pack JSON cannot
|
|
351
|
+
// write outside content/ via an absolute / `..` / null-byte path.
|
|
352
|
+
const outPath = containedOutPath(packName, record[pathField], cfg);
|
|
207
353
|
targets.push([outPath, docRenderer(record, cfg)]);
|
|
208
354
|
}
|
|
209
355
|
return targets;
|
|
@@ -218,7 +364,7 @@ export function collectTargets(packFilter) {
|
|
|
218
364
|
throw new Error(`pack source not found: ${cfg.source}`);
|
|
219
365
|
}
|
|
220
366
|
const pack = JSON.parse(readFileSync(cfg.source, "utf8"));
|
|
221
|
-
for (const [outPath, text] of targetsForPack(pack, cfg)) {
|
|
367
|
+
for (const [outPath, text] of targetsForPack(name, pack, cfg)) {
|
|
222
368
|
targets.push([name, outPath, text]);
|
|
223
369
|
}
|
|
224
370
|
}
|
|
@@ -2,6 +2,16 @@
|
|
|
2
2
|
export declare const SUSPICIOUS_TOKENS: readonly string[];
|
|
3
3
|
export declare const QUARANTINE_FENCE_OPEN = "```quarantined";
|
|
4
4
|
export declare const QUARANTINE_FENCE_CLOSE = "```";
|
|
5
|
+
/**
|
|
6
|
+
* Neutralize a body line so it cannot act as a CommonMark fence open/close.
|
|
7
|
+
* Used on content placed inside a ```quarantined wrapper (#2915 / cache-quarantine-03).
|
|
8
|
+
*
|
|
9
|
+
* A nested bare ``` (or longer run, optional ≤3-space indent) would otherwise
|
|
10
|
+
* early-close the outer fence and let following attacker text render outside
|
|
11
|
+
* the quarantined info-string. Break the delimiter run by inserting a backslash
|
|
12
|
+
* after the first fence character: ``` → `\`` , ~~~ → ~\~~ .
|
|
13
|
+
*/
|
|
14
|
+
export declare function neutralizeFenceLine(line: string): string;
|
|
5
15
|
export declare function quarantineBody(rawMd: string): string;
|
|
6
16
|
export declare function main(argv?: string[]): number;
|
|
7
17
|
//# sourceMappingURL=quarantine-ext.d.ts.map
|
|
@@ -28,6 +28,26 @@ export const SUSPICIOUS_TOKENS = [
|
|
|
28
28
|
];
|
|
29
29
|
export const QUARANTINE_FENCE_OPEN = "```quarantined";
|
|
30
30
|
export const QUARANTINE_FENCE_CLOSE = "```";
|
|
31
|
+
/**
|
|
32
|
+
* Neutralize a body line so it cannot act as a CommonMark fence open/close.
|
|
33
|
+
* Used on content placed inside a ```quarantined wrapper (#2915 / cache-quarantine-03).
|
|
34
|
+
*
|
|
35
|
+
* A nested bare ``` (or longer run, optional ≤3-space indent) would otherwise
|
|
36
|
+
* early-close the outer fence and let following attacker text render outside
|
|
37
|
+
* the quarantined info-string. Break the delimiter run by inserting a backslash
|
|
38
|
+
* after the first fence character: ``` → `\`` , ~~~ → ~\~~ .
|
|
39
|
+
*/
|
|
40
|
+
export function neutralizeFenceLine(line) {
|
|
41
|
+
const match = /^( {0,3})(`{3,}|~{3,})(.*)$/.exec(line);
|
|
42
|
+
if (match === null)
|
|
43
|
+
return line;
|
|
44
|
+
const indent = match[1] ?? "";
|
|
45
|
+
const delim = match[2] ?? "";
|
|
46
|
+
const rest = match[3] ?? "";
|
|
47
|
+
// One backslash after the first fence char breaks the delimiter run.
|
|
48
|
+
// Concat (not a template literal) so "\\" is unambiguously a single "\".
|
|
49
|
+
return indent + delim[0] + "\\" + delim.slice(1) + rest;
|
|
50
|
+
}
|
|
31
51
|
function isWordChar(ch) {
|
|
32
52
|
return ((ch >= "A" && ch <= "Z") || (ch >= "a" && ch <= "z") || (ch >= "0" && ch <= "9") || ch === "_");
|
|
33
53
|
}
|
|
@@ -133,14 +153,18 @@ export function quarantineBody(rawMd) {
|
|
|
133
153
|
sectionEnd += 1;
|
|
134
154
|
}
|
|
135
155
|
out.push(QUARANTINE_FENCE_OPEN);
|
|
136
|
-
|
|
156
|
+
// #2915: neutralize nested fence lines so they cannot early-close.
|
|
157
|
+
for (const bodyLine of lines.slice(i, sectionEnd)) {
|
|
158
|
+
out.push(neutralizeFenceLine(bodyLine));
|
|
159
|
+
}
|
|
137
160
|
out.push(QUARANTINE_FENCE_CLOSE);
|
|
138
161
|
i = sectionEnd;
|
|
139
162
|
continue;
|
|
140
163
|
}
|
|
141
164
|
if (isSuspicious(line)) {
|
|
142
165
|
out.push(QUARANTINE_FENCE_OPEN);
|
|
143
|
-
|
|
166
|
+
// #2915: neutralize in case the single line itself is fence-shaped.
|
|
167
|
+
out.push(neutralizeFenceLine(line));
|
|
144
168
|
out.push(QUARANTINE_FENCE_CLOSE);
|
|
145
169
|
i += 1;
|
|
146
170
|
continue;
|
package/dist/policy/index.d.ts
CHANGED
|
@@ -4,6 +4,7 @@ export * from "./capacity.js";
|
|
|
4
4
|
export * from "./decisions.js";
|
|
5
5
|
export * from "./disclosure.js";
|
|
6
6
|
export * from "./host-hooks.js";
|
|
7
|
+
export * from "./no-deft-directive.js";
|
|
7
8
|
export * from "./org-force-on-migration.js";
|
|
8
9
|
export * from "./plan-extensions.js";
|
|
9
10
|
export * from "./policy-invocation.js";
|
package/dist/policy/index.js
CHANGED
|
@@ -12,6 +12,7 @@ export * from "./capacity.js";
|
|
|
12
12
|
export * from "./decisions.js";
|
|
13
13
|
export * from "./disclosure.js";
|
|
14
14
|
export * from "./host-hooks.js";
|
|
15
|
+
export * from "./no-deft-directive.js";
|
|
15
16
|
export * from "./org-force-on-migration.js";
|
|
16
17
|
export * from "./plan-extensions.js";
|
|
17
18
|
export * from "./policy-invocation.js";
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Official per-project opt-out flag for Directive (#2926).
|
|
3
|
+
*
|
|
4
|
+
* Presence of root `.no-deft-directive` means this project does not use
|
|
5
|
+
* Directive. Session ritual, doctor setup pressure, init/update install paths,
|
|
6
|
+
* and setup skills must honor the flag. Empty file or a short comment is enough.
|
|
7
|
+
*
|
|
8
|
+
* Product choices (v1):
|
|
9
|
+
* - Flag is **root-only** (workspace root the tool opened). Nested monorepo
|
|
10
|
+
* package roots are a documented follow-up.
|
|
11
|
+
* - Flag **wins locally** over ambient trusted-org / product-signal force-on.
|
|
12
|
+
* - Flag + deposit (`.deft/core`) is **inconsistent**: doctor **warns**;
|
|
13
|
+
* mutating install/update paths **fail closed**.
|
|
14
|
+
* - Creating the flag does **not** delete an existing deposit.
|
|
15
|
+
*/
|
|
16
|
+
/** Canonical root-only filename (lowercase). Presence = flag. */
|
|
17
|
+
export declare const NO_DEFT_DIRECTIVE_FLAG_NAME = ".no-deft-directive";
|
|
18
|
+
/** One-line operator message when Directive is disabled by the flag. */
|
|
19
|
+
export declare const NO_DEFT_DIRECTIVE_DISABLED_MESSAGE = "Directive disabled via `.no-deft-directive`";
|
|
20
|
+
/**
|
|
21
|
+
* Loud diagnosis when both the opt-out flag and a deposit exist.
|
|
22
|
+
* Product choice (#2926): doctor **warns**; install/update **fail closed**.
|
|
23
|
+
*/
|
|
24
|
+
export declare const NO_DEFT_DIRECTIVE_INCONSISTENT_MESSAGE = "Inconsistent state: `.no-deft-directive` is present but a Directive deposit (`.deft/core`) also exists. Remove the flag to use Directive, or remove the deposit if opt-out is intentional.";
|
|
25
|
+
/** Recorded product choice for inconsistent flag+deposit handling. */
|
|
26
|
+
export declare const NO_DEFT_DIRECTIVE_INCONSISTENT_POLICY: "warn-and-fail-closed";
|
|
27
|
+
export interface NoDeftDirectiveSeams {
|
|
28
|
+
readonly isFile?: (path: string) => boolean;
|
|
29
|
+
readonly isDir?: (path: string) => boolean;
|
|
30
|
+
}
|
|
31
|
+
export interface NoDeftDirectiveState {
|
|
32
|
+
readonly present: boolean;
|
|
33
|
+
readonly flagPath: string;
|
|
34
|
+
readonly depositPresent: boolean;
|
|
35
|
+
/** True when flag and deposit are both present. */
|
|
36
|
+
readonly inconsistent: boolean;
|
|
37
|
+
}
|
|
38
|
+
/** Absolute path to the root flag file. */
|
|
39
|
+
export declare function noDeftDirectiveFlagPath(projectRoot: string): string;
|
|
40
|
+
/**
|
|
41
|
+
* Detect root `.no-deft-directive` and whether a deposit is also present.
|
|
42
|
+
* Presence is file-existence only (empty or short comment OK).
|
|
43
|
+
*/
|
|
44
|
+
export declare function detectNoDeftDirective(projectRoot: string, seams?: NoDeftDirectiveSeams): NoDeftDirectiveState;
|
|
45
|
+
/** True when the root opt-out flag file exists. */
|
|
46
|
+
export declare function isNoDeftDirectivePresent(projectRoot: string, seams?: NoDeftDirectiveSeams): boolean;
|
|
47
|
+
/**
|
|
48
|
+
* Create the root opt-out flag. Optional one-line rationale becomes a `#` comment.
|
|
49
|
+
* Does not remove an existing deposit.
|
|
50
|
+
*/
|
|
51
|
+
export declare function createNoDeftDirectiveFlag(projectRoot: string, options?: {
|
|
52
|
+
rationale?: string;
|
|
53
|
+
}): string;
|
|
54
|
+
/**
|
|
55
|
+
* Remove the root opt-out flag when present.
|
|
56
|
+
* @returns true when a file was removed.
|
|
57
|
+
*/
|
|
58
|
+
export declare function removeNoDeftDirectiveFlag(projectRoot: string): boolean;
|
|
59
|
+
//# sourceMappingURL=no-deft-directive.d.ts.map
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Official per-project opt-out flag for Directive (#2926).
|
|
3
|
+
*
|
|
4
|
+
* Presence of root `.no-deft-directive` means this project does not use
|
|
5
|
+
* Directive. Session ritual, doctor setup pressure, init/update install paths,
|
|
6
|
+
* and setup skills must honor the flag. Empty file or a short comment is enough.
|
|
7
|
+
*
|
|
8
|
+
* Product choices (v1):
|
|
9
|
+
* - Flag is **root-only** (workspace root the tool opened). Nested monorepo
|
|
10
|
+
* package roots are a documented follow-up.
|
|
11
|
+
* - Flag **wins locally** over ambient trusted-org / product-signal force-on.
|
|
12
|
+
* - Flag + deposit (`.deft/core`) is **inconsistent**: doctor **warns**;
|
|
13
|
+
* mutating install/update paths **fail closed**.
|
|
14
|
+
* - Creating the flag does **not** delete an existing deposit.
|
|
15
|
+
*/
|
|
16
|
+
import { existsSync, statSync, unlinkSync, writeFileSync } from "node:fs";
|
|
17
|
+
import { join, resolve } from "node:path";
|
|
18
|
+
import { assertWriteTargetSafe } from "../fs/projection-containment.js";
|
|
19
|
+
import { CANONICAL_INSTALL_ROOT } from "../init-deposit/constants.js";
|
|
20
|
+
/** Canonical root-only filename (lowercase). Presence = flag. */
|
|
21
|
+
export const NO_DEFT_DIRECTIVE_FLAG_NAME = ".no-deft-directive";
|
|
22
|
+
/** One-line operator message when Directive is disabled by the flag. */
|
|
23
|
+
export const NO_DEFT_DIRECTIVE_DISABLED_MESSAGE = "Directive disabled via `.no-deft-directive`";
|
|
24
|
+
/**
|
|
25
|
+
* Loud diagnosis when both the opt-out flag and a deposit exist.
|
|
26
|
+
* Product choice (#2926): doctor **warns**; install/update **fail closed**.
|
|
27
|
+
*/
|
|
28
|
+
export const NO_DEFT_DIRECTIVE_INCONSISTENT_MESSAGE = "Inconsistent state: `.no-deft-directive` is present but a Directive deposit (`.deft/core`) also exists. Remove the flag to use Directive, or remove the deposit if opt-out is intentional.";
|
|
29
|
+
/** Recorded product choice for inconsistent flag+deposit handling. */
|
|
30
|
+
export const NO_DEFT_DIRECTIVE_INCONSISTENT_POLICY = "warn-and-fail-closed";
|
|
31
|
+
function defaultIsFile(path) {
|
|
32
|
+
try {
|
|
33
|
+
return statSync(path).isFile();
|
|
34
|
+
}
|
|
35
|
+
catch {
|
|
36
|
+
return false;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
function defaultIsDir(path) {
|
|
40
|
+
try {
|
|
41
|
+
return statSync(path).isDirectory();
|
|
42
|
+
}
|
|
43
|
+
catch {
|
|
44
|
+
return false;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
/** Absolute path to the root flag file. */
|
|
48
|
+
export function noDeftDirectiveFlagPath(projectRoot) {
|
|
49
|
+
return resolve(projectRoot, NO_DEFT_DIRECTIVE_FLAG_NAME);
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Detect root `.no-deft-directive` and whether a deposit is also present.
|
|
53
|
+
* Presence is file-existence only (empty or short comment OK).
|
|
54
|
+
*/
|
|
55
|
+
export function detectNoDeftDirective(projectRoot, seams = {}) {
|
|
56
|
+
const flagPath = noDeftDirectiveFlagPath(projectRoot);
|
|
57
|
+
const isFile = seams.isFile ?? defaultIsFile;
|
|
58
|
+
const isDir = seams.isDir ?? defaultIsDir;
|
|
59
|
+
const present = isFile(flagPath);
|
|
60
|
+
const depositPresent = isDir(join(projectRoot, CANONICAL_INSTALL_ROOT));
|
|
61
|
+
return {
|
|
62
|
+
present,
|
|
63
|
+
flagPath,
|
|
64
|
+
depositPresent,
|
|
65
|
+
inconsistent: present && depositPresent,
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
/** True when the root opt-out flag file exists. */
|
|
69
|
+
export function isNoDeftDirectivePresent(projectRoot, seams = {}) {
|
|
70
|
+
return detectNoDeftDirective(projectRoot, seams).present;
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* Create the root opt-out flag. Optional one-line rationale becomes a `#` comment.
|
|
74
|
+
* Does not remove an existing deposit.
|
|
75
|
+
*/
|
|
76
|
+
export function createNoDeftDirectiveFlag(projectRoot, options = {}) {
|
|
77
|
+
const path = noDeftDirectiveFlagPath(projectRoot);
|
|
78
|
+
assertWriteTargetSafe(projectRoot, path);
|
|
79
|
+
if (defaultIsDir(path)) {
|
|
80
|
+
throw new Error(`${NO_DEFT_DIRECTIVE_FLAG_NAME} exists as a directory at ${path}; remove it before creating the opt-out flag file.`);
|
|
81
|
+
}
|
|
82
|
+
const rationale = options.rationale?.trim() ?? "";
|
|
83
|
+
const body = rationale.length > 0 ? `# ${rationale}\n` : "";
|
|
84
|
+
writeFileSync(path, body, "utf8");
|
|
85
|
+
return path;
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* Remove the root opt-out flag when present.
|
|
89
|
+
* @returns true when a file was removed.
|
|
90
|
+
*/
|
|
91
|
+
export function removeNoDeftDirectiveFlag(projectRoot) {
|
|
92
|
+
const path = noDeftDirectiveFlagPath(projectRoot);
|
|
93
|
+
if (!existsSync(path)) {
|
|
94
|
+
return false;
|
|
95
|
+
}
|
|
96
|
+
assertWriteTargetSafe(projectRoot, path);
|
|
97
|
+
if (defaultIsDir(path)) {
|
|
98
|
+
throw new Error(`${NO_DEFT_DIRECTIVE_FLAG_NAME} exists as a directory at ${path}; remove the directory manually before enabling Directive.`);
|
|
99
|
+
}
|
|
100
|
+
unlinkSync(path);
|
|
101
|
+
return true;
|
|
102
|
+
}
|
|
103
|
+
//# sourceMappingURL=no-deft-directive.js.map
|
|
@@ -9,6 +9,10 @@ export interface OrgForceOnMarker {
|
|
|
9
9
|
readonly valueFeedback: boolean;
|
|
10
10
|
readonly productSignal: boolean;
|
|
11
11
|
readonly directiveVersion: string;
|
|
12
|
+
/** Pre-apply typed valueFeedback snapshot for verify-on-skip (#2903). */
|
|
13
|
+
readonly previousValueFeedback?: unknown;
|
|
14
|
+
/** Pre-apply typed productSignal snapshot for verify-on-skip (#2903). */
|
|
15
|
+
readonly previousProductSignal?: unknown;
|
|
12
16
|
}
|
|
13
17
|
/** Canonical typed block written by the one-time migration (#2822 Part A). */
|
|
14
18
|
export declare const FORCE_ON_VALUE_FEEDBACK_BLOCK: {
|
|
@@ -17,8 +21,51 @@ export declare const FORCE_ON_VALUE_FEEDBACK_BLOCK: {
|
|
|
17
21
|
readonly sessionLine: true;
|
|
18
22
|
readonly upstreamPrompt: false;
|
|
19
23
|
};
|
|
24
|
+
/** Normalize undefined → null and deep-sort keys so marker snapshots compare stably. */
|
|
25
|
+
export declare function normalizePolicySnapshot(raw: unknown): unknown;
|
|
26
|
+
/**
|
|
27
|
+
* Structural equality for policy snapshots (#2903).
|
|
28
|
+
* Key order independent via sortKeysDeep — discarded-PD recovery must not miss
|
|
29
|
+
* semantically equal blocks written with different JSON key order.
|
|
30
|
+
*/
|
|
31
|
+
export declare function deepEqualPolicySnapshot(a: unknown, b: unknown): boolean;
|
|
20
32
|
/** Read the #2822 migration marker when present and valid. */
|
|
21
33
|
export declare function readOrgForceOnMarker(projectRoot: string): OrgForceOnMarker | null;
|
|
34
|
+
/**
|
|
35
|
+
* Sentinel previous* for an unforced field that had no stored baseline (legacy
|
|
36
|
+
* partial recovery). Must not deep-equal any real policy snapshot — including
|
|
37
|
+
* null/undefined after clear-value-feedback — or the next update would re-force.
|
|
38
|
+
*/
|
|
39
|
+
export declare const ORG_FORCE_ON_NO_BASELINE: {
|
|
40
|
+
readonly "x-directive/org-force-on": "no-baseline";
|
|
41
|
+
};
|
|
42
|
+
/** Classic #2822 pre-migration all-false valueFeedback baseline (statusreport). */
|
|
43
|
+
export declare const PRE_MIGRATION_VALUE_FEEDBACK_BASELINE: {
|
|
44
|
+
readonly enabled: false;
|
|
45
|
+
readonly emitEvents: false;
|
|
46
|
+
readonly sessionLine: false;
|
|
47
|
+
readonly upstreamPrompt: false;
|
|
48
|
+
};
|
|
49
|
+
/** True when current VF looks like discarded pre-migration state (legacy markers). */
|
|
50
|
+
export declare function looksLikePreMigrationValueFeedback(raw: unknown): boolean;
|
|
51
|
+
/** True when current PS looks like discarded pre-migration state (legacy markers). */
|
|
52
|
+
export declare function looksLikePreMigrationProductSignal(raw: unknown): boolean;
|
|
53
|
+
/**
|
|
54
|
+
* Incomplete migration (#2903): marker present, PD still needs force-on, and
|
|
55
|
+
* current typed block still equals the pre-migration snapshot (or legacy marker
|
|
56
|
+
* has no previous* field and current still looks like the classic baseline).
|
|
57
|
+
*
|
|
58
|
+
* Exact restore of the pre-migration snapshot is treated as incomplete by design
|
|
59
|
+
* (company policy #2376 / #2822 / #2903): discarded working-tree force-on and
|
|
60
|
+
* "put the old all-false block back" are not distinguishable without git history,
|
|
61
|
+
* and trusted-org local collection should stay ON. True intentional opt-out must
|
|
62
|
+
* use a shape that differs from previous*, `policy:clear-value-feedback`, or
|
|
63
|
+
* root `.no-deft-directive`. Outbound product-signal still requires D17 consent.
|
|
64
|
+
*
|
|
65
|
+
* Legacy markers without previous*: only re-apply fields that still look like the
|
|
66
|
+
* classic pre-migration baseline so a distinct intentional sibling opt-out is kept.
|
|
67
|
+
*/
|
|
68
|
+
export declare function isIncompleteForceOnField(needsForceOn: boolean, current: unknown, previousSnapshot: unknown | undefined, previousFieldPresent: boolean, field?: "valueFeedback" | "productSignal"): boolean;
|
|
22
69
|
export interface RunOrgForceOnMigrationOptions {
|
|
23
70
|
readonly autoEnable?: OrgAutoEnableOptions;
|
|
24
71
|
readonly actor?: string;
|
|
@@ -33,6 +80,11 @@ export interface OrgForceOnMigrationResult {
|
|
|
33
80
|
/**
|
|
34
81
|
* One-time trusted-org install/upgrade force-on for valueFeedback + productSignal
|
|
35
82
|
* (#2822). Idempotent via durable marker; non-trusted orgs are unchanged.
|
|
83
|
+
*
|
|
84
|
+
* #2903 verify-on-skip: when the marker is present but PROJECT-DEFINITION still
|
|
85
|
+
* matches the pre-migration snapshot (discarded / never-landed force-on write),
|
|
86
|
+
* re-apply. Intentional post-migration shapes that differ from both the previous
|
|
87
|
+
* snapshot and the force-on block keep the skip.
|
|
36
88
|
*/
|
|
37
89
|
export declare function runOrgForceOnMigration(projectRoot: string, options?: RunOrgForceOnMigrationOptions): OrgForceOnMigrationResult;
|
|
38
90
|
/** True when typed valueFeedback matches the #2822 force-on block shape. */
|