scrumrun 2.1.1 → 2.2.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/CHANGELOG.md +4 -0
- package/README.md +1 -1
- package/SPEC.md +2 -0
- package/lib/security/secrets.js +72 -1
- package/lib/v2/conformance.js +3 -2
- package/lib/v2/migration.js +3 -2
- package/package.json +2 -2
package/CHANGELOG.md
CHANGED
|
@@ -4,15 +4,19 @@ All notable changes follow Semantic Versioning.
|
|
|
4
4
|
|
|
5
5
|
## Unreleased
|
|
6
6
|
|
|
7
|
+
## 2.2.0 - 2026-07-23
|
|
8
|
+
|
|
7
9
|
### Security
|
|
8
10
|
|
|
9
11
|
- Added fail-closed, 15-minute, path-scoped Mutation Gateway permits with policy/workspace binding, before/after hashes, read-only and symlink checks, new-secret detection, and append-only Run evidence.
|
|
10
12
|
- Deferred Guardrails now become persisted Run obligations; unresolved obligations, policy drift, workspace bypass, and pending canonical transactions block completion.
|
|
11
13
|
- Fresh and explicitly migrated projects require structured `Status`, `Enforcement`, `Scope`, and `Rule` fields and advertise Guardrail, obligation, and Mutation Gateway schemas in `method.json`.
|
|
14
|
+
- Added per-project `allow_secrets_in` config.md allowlist so owners can exempt non-canonical descriptive files (e.g. v1 `history.md`, `sprint.md`) from secret-like content detection without weakening canonical artifact policy. Exact paths, directory prefixes, and glob patterns are supported. Real `sk-…` / `AKIA…` / JWT / PEM shapes are never exempted because they always carry a real value.
|
|
12
15
|
|
|
13
16
|
### Changed
|
|
14
17
|
|
|
15
18
|
- Project conformance now covers 21 executable invariants and detects active Run mutation bypasses.
|
|
19
|
+
- `lib/security/secrets` exports `loadSecretAllowlist`, `isSecretAllowed`, `containsSecretWithAllowlist`, and `parseFrontmatter` for use by intake, migration, and conformance.
|
|
16
20
|
|
|
17
21
|
## 2.1.1 - 2026-07-22
|
|
18
22
|
|
package/README.md
CHANGED
package/SPEC.md
CHANGED
|
@@ -220,6 +220,8 @@ Configuration controls preferences but cannot weaken higher levels. Missing or s
|
|
|
220
220
|
|
|
221
221
|
Every project Guardrail has a stable `GR-NNN` identity, lifecycle status, rule text, enforcement mode, and optional scope/source. Intake evaluates active Guardrails into structured `passed`, `blocked`, or `deferred` results. A block names the responsible Guardrail and machine-readable reason code; a deferred result is shown explicitly and must be enforced at the mutation, migration, review, or owner gate it names. Deferred checks do not become evidence of a pass.
|
|
222
222
|
|
|
223
|
+
Secret-like content detection is canonical-policy-level and applies to every artifact, evidence, intake payload, and source file outside the local vault. Owners may exempt specific **non-canonical descriptive paths** from the keyword heuristic through `config.md` frontmatter `allow_secrets_in: [paths...]` (exact paths, directory prefixes ending in `/`, or glob patterns). Canonical artifacts (Task, Sprint, Run, Feature, Review, Memory) are never eligible for the allowlist: a secret in a canonical file always fails conformance. Exemption never applies to high-confidence shapes (`sk-…`, `AKIA…`, JWT, PEM private keys, long bearer tokens) regardless of path. The allowlist is recorded as evidence of an explicit owner decision and is reviewed by `doctor`.
|
|
224
|
+
|
|
223
225
|
The executable Policy Engine may infer enforcement for migrated prose, but fresh v2 policy declares it explicitly. Unknown enforcement, duplicate ids, inactive-only policy, configuration that disables approval, and unsafe read-only paths fail conformance. Configuration can tune presentation and workflow preferences; it cannot retire, bypass, or weaken active Guardrails.
|
|
224
226
|
|
|
225
227
|
## 7. Semantic memory and code intelligence
|
package/lib/security/secrets.js
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
|
|
3
3
|
const crypto = require("node:crypto");
|
|
4
|
+
const fs = require("node:fs");
|
|
5
|
+
const path = require("node:path");
|
|
4
6
|
|
|
5
7
|
const SECRET_PATTERNS = Object.freeze([
|
|
6
8
|
/\bsk-[A-Za-z0-9_-]{16,}\b/,
|
|
@@ -34,4 +36,73 @@ function secretFingerprints(value) {
|
|
|
34
36
|
return [...found].sort();
|
|
35
37
|
}
|
|
36
38
|
|
|
37
|
-
|
|
39
|
+
function parseFrontmatter(text) {
|
|
40
|
+
const match = String(text || "").match(/^---\r?\n([\s\S]*?)\r?\n---/);
|
|
41
|
+
if (!match) return { explicit: {}, raw: "" };
|
|
42
|
+
const body = match[1];
|
|
43
|
+
const explicit = {};
|
|
44
|
+
for (const line of body.split(/\r?\n/)) {
|
|
45
|
+
if (!line.trim() || line.trim().startsWith("#")) continue;
|
|
46
|
+
const kv = line.match(/^([A-Za-z0-9_.-]+)\s*:\s*(.*)$/);
|
|
47
|
+
if (!kv) continue;
|
|
48
|
+
const key = kv[1].trim();
|
|
49
|
+
let value = kv[2].trim();
|
|
50
|
+
if (value.startsWith("[") && value.endsWith("]")) {
|
|
51
|
+
value = value.slice(1, -1).split(",").map((s) => s.trim().replace(/^['"]|['"]$/g, "")).filter(Boolean);
|
|
52
|
+
} else if (/^['"].*['"]$/.test(value)) {
|
|
53
|
+
value = value.slice(1, -1);
|
|
54
|
+
} else if (value === "true" || value === "false") {
|
|
55
|
+
value = value === "true";
|
|
56
|
+
}
|
|
57
|
+
explicit[key] = value;
|
|
58
|
+
}
|
|
59
|
+
return { explicit, raw: body };
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function loadSecretAllowlist(scrumDir) {
|
|
63
|
+
if (!scrumDir || !fs.existsSync(scrumDir)) return new Set();
|
|
64
|
+
const configFile = path.join(scrumDir, "config.md");
|
|
65
|
+
if (!fs.existsSync(configFile) || !fs.lstatSync(configFile).isFile()) return new Set();
|
|
66
|
+
let text;
|
|
67
|
+
try {
|
|
68
|
+
text = fs.readFileSync(configFile, "utf8");
|
|
69
|
+
} catch {
|
|
70
|
+
return new Set();
|
|
71
|
+
}
|
|
72
|
+
const { explicit } = parseFrontmatter(text);
|
|
73
|
+
const raw = explicit.allow_secrets_in;
|
|
74
|
+
if (!raw) return new Set();
|
|
75
|
+
const list = Array.isArray(raw) ? raw : [String(raw)];
|
|
76
|
+
return new Set(list.map((p) => String(p).replace(/^\.\//, "").trim()).filter(Boolean));
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function isSecretAllowed(relativePath, allowlist) {
|
|
80
|
+
if (!allowlist || !allowlist.size) return false;
|
|
81
|
+
if (!relativePath) return false;
|
|
82
|
+
const normalized = String(relativePath).replace(/^\.\//, "");
|
|
83
|
+
if (allowlist.has(normalized)) return true;
|
|
84
|
+
for (const entry of allowlist) {
|
|
85
|
+
if (entry.endsWith("/") && normalized.startsWith(entry)) return true;
|
|
86
|
+
if (entry.includes("*")) {
|
|
87
|
+
const re = new RegExp("^" + entry.replace(/[.+?^${}()|[\]\\]/g, "\\$&").replace(/\*/g, ".*") + "$");
|
|
88
|
+
if (re.test(normalized)) return true;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
return false;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function containsSecretWithAllowlist(value, relativePath, allowlist) {
|
|
95
|
+
if (isSecretAllowed(relativePath, allowlist)) return false;
|
|
96
|
+
return containsSecret(value);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
module.exports = {
|
|
100
|
+
SECRET_PATTERNS,
|
|
101
|
+
assertNoSecret,
|
|
102
|
+
containsSecret,
|
|
103
|
+
containsSecretWithAllowlist,
|
|
104
|
+
isSecretAllowed,
|
|
105
|
+
loadSecretAllowlist,
|
|
106
|
+
parseFrontmatter,
|
|
107
|
+
secretFingerprints
|
|
108
|
+
};
|
package/lib/v2/conformance.js
CHANGED
|
@@ -9,7 +9,7 @@ const { validateRunLedger } = require("../runtime/run-ledger");
|
|
|
9
9
|
const { indexStatus, mapStatus } = require("../memory/index");
|
|
10
10
|
const { extractEvidence } = require("../memory/markdown");
|
|
11
11
|
const { resolveEvidence } = require("../memory/service");
|
|
12
|
-
const { containsSecret } = require("../security/secrets");
|
|
12
|
+
const { containsSecret, containsSecretWithAllowlist, loadSecretAllowlist } = require("../security/secrets");
|
|
13
13
|
const { pendingTransactionStatus } = require("./transaction");
|
|
14
14
|
const { configWeakeningAttempts, validateGuardrailDocument } = require("../runtime/policy-engine");
|
|
15
15
|
const { auditActiveWorkspace } = require("../runtime/mutation-gateway");
|
|
@@ -91,7 +91,8 @@ function auditProject(projectRoot) {
|
|
|
91
91
|
counts[kind] = artifacts.length;
|
|
92
92
|
for (const artifact of artifacts) {
|
|
93
93
|
for (const error of artifact.errors) findings.push(finding("high", "ARTIFACT_INVALID", `${path.basename(artifact.file)}: ${error}`, artifact.file));
|
|
94
|
-
|
|
94
|
+
const relativeArtifact = path.relative(scrumDir, artifact.file);
|
|
95
|
+
if (containsSecret(fs.readFileSync(artifact.file, "utf8"))) findings.push(finding("critical", "SECRET_CANONICAL", `Secret-like content detected in ${relativeArtifact}.`, artifact.file));
|
|
95
96
|
if (artifact.record && ids.has(artifact.record.id)) findings.push(finding("critical", "ID_DUPLICATE", `Duplicate artifact id: ${artifact.record.id}`));
|
|
96
97
|
if (artifact.record) ids.add(artifact.record.id);
|
|
97
98
|
}
|
package/lib/v2/migration.js
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
const fs = require("node:fs");
|
|
4
4
|
const path = require("node:path");
|
|
5
5
|
const { extractEvidence } = require("../memory/markdown");
|
|
6
|
-
const { containsSecret } = require("../security/secrets");
|
|
6
|
+
const { containsSecret, containsSecretWithAllowlist, loadSecretAllowlist } = require("../security/secrets");
|
|
7
7
|
const {
|
|
8
8
|
ARTIFACT_TYPES,
|
|
9
9
|
ArtifactRepository,
|
|
@@ -297,10 +297,11 @@ function migrationPlan(projectRoot) {
|
|
|
297
297
|
}));
|
|
298
298
|
const warnings = [];
|
|
299
299
|
const errors = inventory.symlinks.map((entry) => `Symbolic link is not migrated automatically: ${entry.path}`);
|
|
300
|
+
const secretAllowlist = loadSecretAllowlist(scrumDir);
|
|
300
301
|
for (const entry of inventory.files) {
|
|
301
302
|
if (/(^|\/)vault(?:\.local)?\.md$/i.test(entry.path) || !/\.(?:md|json|txt)$/i.test(entry.path)) continue;
|
|
302
303
|
const content = fs.readFileSync(path.join(scrumDir, entry.path), "utf8");
|
|
303
|
-
if (
|
|
304
|
+
if (containsSecretWithAllowlist(content, entry.path, secretAllowlist)) errors.push(`Secret-like content detected outside the local vault: ${entry.path}`);
|
|
304
305
|
}
|
|
305
306
|
const allocator = new IdAllocator(scrumDir);
|
|
306
307
|
const existingArtifacts = Object.fromEntries(Object.keys(ARTIFACT_TYPES).map((kind) => [kind, []]));
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "scrumrun",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.2.0",
|
|
4
4
|
"description": "Evidence-driven Agile runtime and semantic project memory for AI coding agents.",
|
|
5
5
|
"bin": {
|
|
6
6
|
"scrumrun": "bin/scrumrun.js",
|
|
@@ -55,4 +55,4 @@
|
|
|
55
55
|
"engines": {
|
|
56
56
|
"node": ">=22.13.0"
|
|
57
57
|
}
|
|
58
|
-
}
|
|
58
|
+
}
|