@codyswann/lisa 2.311.14 → 2.313.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.
Files changed (69) hide show
  1. package/dist/core/upstream-evidence-manifest.d.ts.map +1 -1
  2. package/dist/core/upstream-evidence-manifest.js +17 -1
  3. package/dist/core/upstream-evidence-manifest.js.map +1 -1
  4. package/package.json +1 -1
  5. package/plugins/lisa/.claude-plugin/plugin.json +1 -1
  6. package/plugins/lisa/.codex-plugin/plugin.json +1 -1
  7. package/plugins/lisa/.codex-plugin/skills/lisa-secrets-access/SKILL.md +122 -0
  8. package/plugins/lisa/.codex-plugin/skills/lisa-secrets-access/agents/openai.yaml +4 -0
  9. package/plugins/lisa/.codex-plugin/skills/lisa-secrets-access/scripts/resolve-secret.mjs +261 -0
  10. package/plugins/lisa/skills/lisa-secrets-access/SKILL.md +122 -0
  11. package/plugins/lisa/skills/lisa-secrets-access/agents/openai.yaml +4 -0
  12. package/plugins/lisa/skills/lisa-secrets-access/scripts/resolve-secret.mjs +261 -0
  13. package/plugins/lisa-agy/plugin.json +1 -1
  14. package/plugins/lisa-agy/skills/lisa-secrets-access/SKILL.md +122 -0
  15. package/plugins/lisa-agy/skills/lisa-secrets-access/scripts/resolve-secret.mjs +261 -0
  16. package/plugins/lisa-cdk/.claude-plugin/plugin.json +1 -1
  17. package/plugins/lisa-cdk/.codex-plugin/plugin.json +1 -1
  18. package/plugins/lisa-cdk-agy/plugin.json +1 -1
  19. package/plugins/lisa-cdk-copilot/.claude-plugin/plugin.json +1 -1
  20. package/plugins/lisa-cdk-cursor/.claude-plugin/plugin.json +1 -1
  21. package/plugins/lisa-copilot/.claude-plugin/plugin.json +1 -1
  22. package/plugins/lisa-copilot/skills/lisa-secrets-access/SKILL.md +122 -0
  23. package/plugins/lisa-copilot/skills/lisa-secrets-access/scripts/resolve-secret.mjs +261 -0
  24. package/plugins/lisa-cursor/.claude-plugin/plugin.json +1 -1
  25. package/plugins/lisa-cursor/skills/lisa-secrets-access/SKILL.md +122 -0
  26. package/plugins/lisa-cursor/skills/lisa-secrets-access/scripts/resolve-secret.mjs +261 -0
  27. package/plugins/lisa-expo/.claude-plugin/plugin.json +1 -1
  28. package/plugins/lisa-expo/.codex-plugin/plugin.json +1 -1
  29. package/plugins/lisa-expo-agy/plugin.json +1 -1
  30. package/plugins/lisa-expo-copilot/.claude-plugin/plugin.json +1 -1
  31. package/plugins/lisa-expo-cursor/.claude-plugin/plugin.json +1 -1
  32. package/plugins/lisa-harper-fabric/.claude-plugin/plugin.json +1 -1
  33. package/plugins/lisa-harper-fabric/.codex-plugin/plugin.json +1 -1
  34. package/plugins/lisa-harper-fabric-agy/plugin.json +1 -1
  35. package/plugins/lisa-harper-fabric-copilot/.claude-plugin/plugin.json +1 -1
  36. package/plugins/lisa-harper-fabric-cursor/.claude-plugin/plugin.json +1 -1
  37. package/plugins/lisa-nestjs/.claude-plugin/plugin.json +1 -1
  38. package/plugins/lisa-nestjs/.codex-plugin/plugin.json +1 -1
  39. package/plugins/lisa-nestjs-agy/plugin.json +1 -1
  40. package/plugins/lisa-nestjs-copilot/.claude-plugin/plugin.json +1 -1
  41. package/plugins/lisa-nestjs-cursor/.claude-plugin/plugin.json +1 -1
  42. package/plugins/lisa-openclaw/.claude-plugin/plugin.json +1 -1
  43. package/plugins/lisa-openclaw/.codex-plugin/plugin.json +1 -1
  44. package/plugins/lisa-openclaw-agy/plugin.json +1 -1
  45. package/plugins/lisa-openclaw-copilot/.claude-plugin/plugin.json +1 -1
  46. package/plugins/lisa-openclaw-cursor/.claude-plugin/plugin.json +1 -1
  47. package/plugins/lisa-phaser/.claude-plugin/plugin.json +1 -1
  48. package/plugins/lisa-phaser/.codex-plugin/plugin.json +1 -1
  49. package/plugins/lisa-phaser-agy/plugin.json +1 -1
  50. package/plugins/lisa-phaser-copilot/.claude-plugin/plugin.json +1 -1
  51. package/plugins/lisa-phaser-cursor/.claude-plugin/plugin.json +1 -1
  52. package/plugins/lisa-rails/.claude-plugin/plugin.json +1 -1
  53. package/plugins/lisa-rails/.codex-plugin/plugin.json +1 -1
  54. package/plugins/lisa-rails-agy/plugin.json +1 -1
  55. package/plugins/lisa-rails-copilot/.claude-plugin/plugin.json +1 -1
  56. package/plugins/lisa-rails-cursor/.claude-plugin/plugin.json +1 -1
  57. package/plugins/lisa-typescript/.claude-plugin/plugin.json +1 -1
  58. package/plugins/lisa-typescript/.codex-plugin/plugin.json +1 -1
  59. package/plugins/lisa-typescript-agy/plugin.json +1 -1
  60. package/plugins/lisa-typescript-copilot/.claude-plugin/plugin.json +1 -1
  61. package/plugins/lisa-typescript-cursor/.claude-plugin/plugin.json +1 -1
  62. package/plugins/lisa-wiki/.claude-plugin/plugin.json +1 -1
  63. package/plugins/lisa-wiki/.codex-plugin/plugin.json +1 -1
  64. package/plugins/lisa-wiki-agy/plugin.json +1 -1
  65. package/plugins/lisa-wiki-copilot/.claude-plugin/plugin.json +1 -1
  66. package/plugins/lisa-wiki-cursor/.claude-plugin/plugin.json +1 -1
  67. package/plugins/src/base/skills/lisa-secrets-access/SKILL.md +122 -0
  68. package/plugins/src/base/skills/lisa-secrets-access/scripts/resolve-secret.mjs +261 -0
  69. package/ui/index.html +74 -1
@@ -0,0 +1,122 @@
1
+ ---
2
+ name: lisa-secrets-access
3
+ description: "Vendor-neutral access layer for secrets. Every skill and script that needs an API key MUST resolve it through this skill rather than reading a keychain, an .env file, or a provider CLI directly. Resolves the environment first so CI injection wins, then the configured provider — Bitwarden Secrets Manager, 1Password, AWS Secrets Manager, Doppler or Vault — looked up by key name. Enforces one store per secret, reads usage metadata from the provider's own note field, and never writes."
4
+ allowed-tools: ["Bash", "Read", "Skill"]
5
+ ---
6
+
7
+ # Secrets Access: $ARGUMENTS
8
+
9
+ Single chokepoint for reading credentials. Caller skills MUST go through this — they MUST NOT read the OS keychain, parse a `.env`, or invoke a provider CLI themselves.
10
+
11
+ The rule this exists to enforce: **a secret lives in exactly one store.** Every local cache is a copy that will eventually drift from its source, and a drifted copy is indistinguishable from a valid one until something fails in production.
12
+
13
+ ## Invocation contract
14
+
15
+ ```text
16
+ operation: get name: ATTIO_API_KEY
17
+ operation: list # names only, never values
18
+ operation: describe name: ATTIO_API_KEY # the usage note, not the value
19
+ operation: verify # every declared secret resolves
20
+ ```
21
+
22
+ `get` returns the value on stdout and nothing else. `list` and `describe` never emit a secret value.
23
+
24
+ ## Configuration
25
+
26
+ In `.lisa.config.json`:
27
+
28
+ ```json
29
+ {
30
+ "secrets": {
31
+ "provider": "bitwarden",
32
+ "bootstrap": { "sources": ["env", "keychain"], "key": "BWS_ACCESS_TOKEN" },
33
+ "require": ["ATTIO_API_KEY", "SLACK_WEBHOOK_URL"]
34
+ }
35
+ }
36
+ ```
37
+
38
+ **`provider`** — `bitwarden` | `1password` | `aws` | `doppler` | `vault` | `env`.
39
+
40
+ **`bootstrap`** — how to obtain the one credential that unlocks the rest. `sources` is ordered: environment first, so CI injection wins over any local copy. This is the **only** credential permitted in a keychain; it is a bootstrap, not a cache.
41
+
42
+ **`require`** — optional. Omit it and every secret the provider grants is available, which is correct when the provider already scopes access per project. Present, it narrows to exactly those names **and asserts them**: a listed name that does not resolve is a startup error, not a late surprise. Use it when a repo needs a subset of a deliberately broader project.
43
+
44
+ There is no map of secret IDs, deliberately. Copying an ID per secret is the same duplication in a smaller costume, and lookup is by name.
45
+
46
+ ## The naming convention this rests on
47
+
48
+ **A secret's key is the exact environment-variable name.** No inference, no fuzzy matching, no case folding. A secret named `attio-prod` will not resolve for `ATTIO_API_KEY`, and the error says so rather than silently returning nothing.
49
+
50
+ Enforce it: `doctor` should warn on any key that is not a valid `UPPER_SNAKE_CASE` identifier.
51
+
52
+ ## Workflow
53
+
54
+ ### Step 1 — Environment first
55
+
56
+ If `$<NAME>` is set and non-empty, return it. This is how CI injects secrets, and it means a scheduled run never touches the provider or a local store.
57
+
58
+ ### Step 2 — Bootstrap
59
+
60
+ Resolve the bootstrap credential by walking `bootstrap.sources` in order. Fail with an actionable message naming where it was looked for.
61
+
62
+ ### Step 3 — Provider lookup by name
63
+
64
+ | Provider | Read |
65
+ | --- | --- |
66
+ | `bitwarden` | `bws secret list` → index by `key` |
67
+ | `1password` | `op read "op://<vault>/<name>/credential"` |
68
+ | `aws` | `aws secretsmanager get-secret-value --secret-id <name>` |
69
+ | `doppler` | `doppler secrets get <name> --plain` |
70
+ | `vault` | `vault kv get -field=<name> <path>` |
71
+ | `env` | environment only; the provider is the environment |
72
+
73
+ Cache **in-process only**. Never write a resolved value to disk — that recreates the problem this skill exists to remove.
74
+
75
+ ### Step 4 — Fail loudly and usefully
76
+
77
+ A missing name reports what *is* visible, so the caller can see immediately whether the secret is absent or merely misnamed:
78
+
79
+ ```text
80
+ GOOGLE_SERVICE_ACCOUNT_JSON is not available to this account.
81
+ Visible: APOLLO_API_KEY, ATTIO_API_KEY, SLACK_WEBHOOK_URL
82
+ A secret's key must be the exact environment variable name.
83
+ ```
84
+
85
+ ## Usage metadata lives on the secret
86
+
87
+ Every provider has a description field — Bitwarden `note`, 1Password notes, AWS `Description`, Doppler notes, Vault custom metadata. **That is where a secret's usage documentation belongs**, not in a config file: a note travels with the secret and therefore cannot drift from it, which is precisely the property config lacks.
88
+
89
+ Format — first line prose, then `key: value` lines:
90
+
91
+ ```text
92
+ Attio CRM - system of record for the sales funnel.
93
+ scope: object_configuration, record, list_entry - read-write
94
+ owner: <name>
95
+ ci: yes - injected by <mechanism>
96
+ docs: <path>
97
+ ```
98
+
99
+ `describe` returns this. When a note is empty, **infer purpose from the name, mark it inferred, and report the gap** — infer *and* warn, never instead of. A silent fallback that works well enough guarantees the notes stay empty forever.
100
+
101
+ **An inferred mapping must never authorise a write.** It orients a reader; it does not pick which credential calls a production API. `ATTIO_API_KEY` versus `ATTIO_API_KEY_STAGING` is exactly the guess that silently writes to the wrong system.
102
+
103
+ ## This skill never writes
104
+
105
+ No create, no update, no rotate. Writing secrets or their notes requires an authority a CI credential should not hold, and a read-only path cannot be turned against the vault if it leaks.
106
+
107
+ Where a rotating credential must persist a new value — an OAuth refresh token that the issuer replaces on every use — that is a **separate, deliberately-scoped writer**, and exactly one process may hold that loop. Two refreshers race, and each invalidates the other's token silently.
108
+
109
+ ## Doctor checks worth wiring
110
+
111
+ - Every name in `require` resolves.
112
+ - Every key matches `^[A-Z][A-Z0-9_]*$`.
113
+ - No secret has an empty note.
114
+ - **No secret is readable from two stores.** A value present in both the provider and a local cache is not a duplicate — it is **two live credentials**, one of which is untracked. This is the check most worth having: it catches drift before a deletion turns the forgotten copy into an orphan nobody can revoke.
115
+
116
+ ## Rules
117
+
118
+ 1. **Never read a keychain, `.env`, or provider CLI outside this skill.** One chokepoint is what makes the single-store rule enforceable.
119
+ 2. **Never log a secret value.** Print a length or a hash prefix when proving identity.
120
+ 3. **Never write a resolved value to disk**, including "temporary" files.
121
+ 4. **Verify a credential when it is stored, not when it is first used.** An unverified credential is indistinguishable from a broken one, and the gap between the two is measured in weeks.
122
+ 5. **Treat a mismatch as stop-and-ask.** If a value differs between two places, they are two live credentials — not a stale copy to be discarded. Deleting the one you cannot verify leaves a working credential that no record accounts for.
@@ -0,0 +1,261 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Resolve a secret through the configured provider.
4
+ *
5
+ * The executable half of `lisa-secrets-access`. It exists as a CLI rather than
6
+ * a library because the callers are polyglot — Python scripts, shell, and
7
+ * TypeScript all need the same answer, and a subprocess is the only interface
8
+ * all three share without duplicating the logic three times.
9
+ *
10
+ * Resolution order is environment, then provider. The environment comes first
11
+ * so a CI run — where secrets are injected by the pipeline — never reaches for
12
+ * a provider or a local store at all.
13
+ *
14
+ * Values are written to stdout with no trailing newline and nothing else, so
15
+ * `$(resolve-secret.mjs get NAME)` is safe. Diagnostics go to stderr.
16
+ *
17
+ * Usage:
18
+ * resolve-secret.mjs get NAME
19
+ * resolve-secret.mjs list
20
+ * resolve-secret.mjs describe NAME
21
+ * resolve-secret.mjs verify
22
+ */
23
+
24
+ import { execFileSync } from "node:child_process";
25
+ import { existsSync, readFileSync } from "node:fs";
26
+ import { join } from "node:path";
27
+
28
+ /** Config defaults when `.lisa.config.json` carries no `secrets` block. */
29
+ const DEFAULTS = {
30
+ provider: "env",
31
+ bootstrap: { sources: ["env"], key: null },
32
+ require: null,
33
+ };
34
+
35
+ /**
36
+ * Read the `secrets` block from `.lisa.config.json`.
37
+ * @param {string} cwd Directory to look in.
38
+ * @returns {{provider: string, bootstrap: {sources: string[], key: string|null}, require: string[]|null}}
39
+ */
40
+ export function readConfig(cwd = process.cwd()) {
41
+ const path = join(cwd, ".lisa.config.json");
42
+ if (!existsSync(path)) return DEFAULTS;
43
+ try {
44
+ const cfg = JSON.parse(readFileSync(path, "utf8")).secrets;
45
+ if (!cfg) return DEFAULTS;
46
+ return {
47
+ provider: cfg.provider ?? DEFAULTS.provider,
48
+ bootstrap: { ...DEFAULTS.bootstrap, ...(cfg.bootstrap ?? {}) },
49
+ require: cfg.require ?? null,
50
+ };
51
+ } catch (err) {
52
+ throw new Error(`.lisa.config.json is not readable: ${err.message}`);
53
+ }
54
+ }
55
+
56
+ /**
57
+ * Obtain the one credential that unlocks the provider.
58
+ *
59
+ * Walks `sources` in order. This is the only credential permitted in an OS
60
+ * keychain — it is a bootstrap, not a cached copy of anything.
61
+ * @param {{sources: string[], key: string|null}} bootstrap Bootstrap config.
62
+ * @returns {string|null} The token, or null when the provider needs none.
63
+ */
64
+ function bootstrapToken(bootstrap) {
65
+ if (!bootstrap.key) return null;
66
+ for (const source of bootstrap.sources) {
67
+ if (source === "env") {
68
+ const v = (process.env[bootstrap.key] ?? "").trim();
69
+ if (v) return v;
70
+ }
71
+ if (source === "keychain" && process.platform === "darwin") {
72
+ try {
73
+ const v = execFileSync(
74
+ "security",
75
+ [
76
+ "find-generic-password",
77
+ "-s",
78
+ bootstrap.key,
79
+ "-a",
80
+ process.env.USER ?? "",
81
+ "-w",
82
+ ],
83
+ { encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] }
84
+ ).trim();
85
+ if (v) return v;
86
+ } catch {
87
+ /* not present in the keychain; try the next source */
88
+ }
89
+ }
90
+ }
91
+ throw new Error(
92
+ `${bootstrap.key} not found in: ${bootstrap.sources.join(", ")}.\n` +
93
+ `It is the bootstrap credential — without it no other secret can be read.`
94
+ );
95
+ }
96
+
97
+ /**
98
+ * Fetch every secret the provider grants, keyed by name.
99
+ *
100
+ * Deliberately fetches all rather than one: the provider already scopes what
101
+ * this caller may see, so the set it returns *is* the permitted set, and
102
+ * re-stating that as a list in config would duplicate a boundary the provider
103
+ * already enforces.
104
+ * @param {ReturnType<typeof readConfig>} cfg Resolved config.
105
+ * @returns {Map<string, {value: string, note: string}>} Secrets by name.
106
+ */
107
+ function fetchAll(cfg) {
108
+ const out = new Map();
109
+ const token = bootstrapToken(cfg.bootstrap);
110
+ const env = { ...process.env };
111
+ if (cfg.bootstrap.key && token) env[cfg.bootstrap.key] = token;
112
+
113
+ switch (cfg.provider) {
114
+ case "env":
115
+ for (const [k, v] of Object.entries(process.env)) {
116
+ if (/^[A-Z][A-Z0-9_]*$/.test(k))
117
+ out.set(k, { value: v ?? "", note: "" });
118
+ }
119
+ return out;
120
+
121
+ case "bitwarden": {
122
+ const raw = execFileSync("bws", ["secret", "list", "--output", "json"], {
123
+ encoding: "utf8",
124
+ env,
125
+ stdio: ["ignore", "pipe", "pipe"],
126
+ });
127
+ for (const s of JSON.parse(raw || "[]")) {
128
+ out.set(s.key, { value: s.value, note: s.note ?? "" });
129
+ }
130
+ return out;
131
+ }
132
+
133
+ case "doppler": {
134
+ const raw = execFileSync(
135
+ "doppler",
136
+ ["secrets", "download", "--no-file", "--format", "json"],
137
+ {
138
+ encoding: "utf8",
139
+ env,
140
+ stdio: ["ignore", "pipe", "pipe"],
141
+ }
142
+ );
143
+ for (const [k, v] of Object.entries(JSON.parse(raw || "{}"))) {
144
+ out.set(k, { value: String(v), note: "" });
145
+ }
146
+ return out;
147
+ }
148
+
149
+ default:
150
+ throw new Error(
151
+ `provider "${cfg.provider}" has no bulk read implemented yet.\n` +
152
+ `Add one in resolve-secret.mjs — see the dispatch table in SKILL.md.`
153
+ );
154
+ }
155
+ }
156
+
157
+ /**
158
+ * Resolve one secret: environment first, then the provider.
159
+ * @param {string} name Environment-variable-style key.
160
+ * @param {ReturnType<typeof readConfig>} [cfg] Resolved config.
161
+ * @returns {string} The secret value.
162
+ */
163
+ export function get(name, cfg = readConfig()) {
164
+ const fromEnv = (process.env[name] ?? "").trim();
165
+ if (fromEnv) return fromEnv;
166
+
167
+ // A `require` list is an assertion, not just a filter: naming a secret
168
+ // declares that this project needs it, so asking for one outside the list is
169
+ // a configuration error rather than a lookup miss.
170
+ if (cfg.require && !cfg.require.includes(name)) {
171
+ throw new Error(
172
+ `${name} is not declared in secrets.require.\n` +
173
+ `Declared: ${cfg.require.join(", ") || "(none)"}`
174
+ );
175
+ }
176
+
177
+ const all = fetchAll(cfg);
178
+ const hit = all.get(name);
179
+ if (!hit || !hit.value) {
180
+ throw new Error(
181
+ `${name} is not available to this account.\n` +
182
+ `Visible: ${[...all.keys()].sort().join(", ") || "(none)"}\n` +
183
+ `A secret's key must be the exact environment variable name.`
184
+ );
185
+ }
186
+ return hit.value;
187
+ }
188
+
189
+ function main() {
190
+ const [op, name] = process.argv.slice(2);
191
+ const cfg = readConfig();
192
+
193
+ if (op === "get") {
194
+ if (!name) throw new Error("usage: resolve-secret.mjs get NAME");
195
+ process.stdout.write(get(name, cfg));
196
+ return;
197
+ }
198
+ if (op === "list") {
199
+ // Names only. This command must never be able to leak a value.
200
+ console.log([...fetchAll(cfg).keys()].sort().join("\n"));
201
+ return;
202
+ }
203
+ if (op === "describe") {
204
+ if (!name) throw new Error("usage: resolve-secret.mjs describe NAME");
205
+ const hit = fetchAll(cfg).get(name);
206
+ if (hit) {
207
+ console.log(
208
+ hit.note || `(no note — purpose inferred from the name: ${name})`
209
+ );
210
+ return;
211
+ }
212
+ // Environment-only is a legitimate state, not an error: CI injects secrets
213
+ // that never appear in the provider listing. Say so rather than claiming
214
+ // the secret does not exist, and never print the value.
215
+ if ((process.env[name] ?? "").trim()) {
216
+ console.log(
217
+ `(set in the environment; no provider entry, so no note is available)`
218
+ );
219
+ return;
220
+ }
221
+ throw new Error(`${name} not found in the environment or the provider`);
222
+ }
223
+ if (op === "verify") {
224
+ const all = fetchAll(cfg);
225
+ const names = cfg.require ?? [...all.keys()];
226
+ let bad = 0;
227
+ for (const n of names) {
228
+ // Mirror get()'s environment-first order. Checking only the provider
229
+ // reports MISSING for every secret in CI, where the pipeline injects
230
+ // them as environment variables — a false negative precisely where
231
+ // verification matters most.
232
+ const fromEnv = Boolean((process.env[n] ?? "").trim());
233
+ const entry = all.get(n);
234
+ const ok = fromEnv || Boolean(entry?.value);
235
+ const named = /^[A-Z][A-Z0-9_]*$/.test(n);
236
+ const noted = Boolean(entry?.note);
237
+ if (!ok || !named) bad += 1;
238
+ const source = fromEnv
239
+ ? "env "
240
+ : entry?.value
241
+ ? "provider"
242
+ : " ";
243
+ console.log(
244
+ ` ${n.padEnd(30)} ${ok ? "resolves" : "MISSING "} ${source} ` +
245
+ `${named ? "name ok" : "NAME NOT UPPER_SNAKE"} ${noted ? "noted" : "no note"}`
246
+ );
247
+ }
248
+ if (bad) throw new Error(`${bad} secret(s) failed verification`);
249
+ return;
250
+ }
251
+ throw new Error("usage: resolve-secret.mjs get|list|describe|verify [NAME]");
252
+ }
253
+
254
+ if (import.meta.url === `file://${process.argv[1]}`) {
255
+ try {
256
+ main();
257
+ } catch (err) {
258
+ console.error(err.message);
259
+ process.exit(1);
260
+ }
261
+ }
package/ui/index.html CHANGED
@@ -5688,6 +5688,25 @@
5688
5688
  ],
5689
5689
  viaValue: "license-checker",
5690
5690
  },
5691
+ {
5692
+ q: "What catches licensed or proprietary code reproduced directly into your source?",
5693
+ help: "TASC SI10. The allowlist above governs declared dependencies — code a model reproduces from memory, or an agent copies out of a repository it fetched while researching, carries no lockfile entry and no SBOM line, so every scanner above is blind to it. This gate detects verbatim and near-verbatim reuse and holds the merge until the source's license is identified and its obligations discharged. Attribution survives inlining: a permissive license on the allowlist is a permission with conditions, not an absence of them, and a notice file assembled by hand is a control nobody has measured.",
5694
+ kind: "select",
5695
+ options: [
5696
+ "Similarity gate on changed code, with attribution notices generated mechanically",
5697
+ "Similarity scanning, attribution handled by hand",
5698
+ "We rely on the model vendor's filter",
5699
+ "Nothing — review would have to notice",
5700
+ ],
5701
+ via: [
5702
+ "SCANOSS",
5703
+ "FOSSA",
5704
+ "Black Duck",
5705
+ "ScanCode",
5706
+ "Vendor-side filter",
5707
+ "None",
5708
+ ],
5709
+ },
5691
5710
  ],
5692
5711
  },
5693
5712
  {
@@ -5725,6 +5744,24 @@
5725
5744
  "Anything",
5726
5745
  ],
5727
5746
  },
5747
+ {
5748
+ q: "Which codebases may an agent draw from — and what stops one client's source reaching another's?",
5749
+ help: "TASC SI10. Reading the web is an injection surface; reading source is a licensing and confidentiality one. The permissible-source boundary — which repositories, corpora and engagement contexts an agent may draw from — has to be enforced by mechanism rather than instruction, because an instruction not to reuse another client's code is a rule with an unmeasured breach rate (AC5.7). Persisted memory and learnings carry source across that boundary exactly as context does, and the harm here is contractual rather than reputational.",
5750
+ kind: "select",
5751
+ options: [
5752
+ "Declared boundary, enforced mechanically, sources recorded per change",
5753
+ "Declared boundary, enforced by instruction",
5754
+ "Isolation is per-repo by convention",
5755
+ "No declared boundary",
5756
+ ],
5757
+ via: [
5758
+ "Per-engagement workspace isolation",
5759
+ "Sandbox / container scoping",
5760
+ "Repository allowlist",
5761
+ "Context provenance log",
5762
+ "None",
5763
+ ],
5764
+ },
5728
5765
  {
5729
5766
  q: "Can an agent add a new dependency unreviewed?",
5730
5767
  help: "Models hallucinate plausible package names and attackers register them (slopsquatting), alongside classic typosquats. An agent that can install unreviewed is the softest supply-chain target there is.",
@@ -5966,7 +6003,7 @@
5966
6003
  ],
5967
6004
  },
5968
6005
  {
5969
- label: "Review & merge (AC8.1 · AC5 · AC5.7 · AC8.8)",
6006
+ label: "Review & merge (AC8.1 · AC5 · AC5.7–AC5.9 · AC8.8)",
5970
6007
  items: [
5971
6008
  {
5972
6009
  q: "What reviews every diff before merge?",
@@ -6052,6 +6089,42 @@
6052
6089
  "We assume the instructions are followed",
6053
6090
  ],
6054
6091
  },
6092
+ {
6093
+ q: "Where do your control obligations live — and do the gates come from there?",
6094
+ help: "TASC AC5.8. A rule transcribed into a CI config, a lint rule and an instruction file has three sources of truth and therefore none, and nothing can detect drift between them. Each obligation should be one versioned, machine-readable artifact naming its enforcement tier and the independent mechanism that measures it — a coverage floor that doesn't say which system computes coverage has declared an intention, not a control — with every enforcement point and every agent-facing rendering generated from it.",
6095
+ kind: "select",
6096
+ options: [
6097
+ "Declared as machine-readable artifacts, with enforcement points and agent context generated from them",
6098
+ "Machine-readable, but each enforcement point is configured separately",
6099
+ "Written as prose policy documents",
6100
+ "Only as whatever each tool happens to be configured to do",
6101
+ ],
6102
+ via: [
6103
+ "Policy-as-code in the repo",
6104
+ "OPA / Rego",
6105
+ "Generated CI configuration",
6106
+ "In-house policy engine",
6107
+ "None",
6108
+ ],
6109
+ },
6110
+ {
6111
+ q: "Can you enumerate every control, where it is enforced, and when it last fired?",
6112
+ help: "TASC AC5.9 — the first question an attestor asks, and the one a factory with its control configuration scattered across a pipeline cannot answer. The register carries each obligation's enforcement tier, its measuring mechanism, its last exercised evidence and freshness, measured adherence for the advisory ones, and who answers for it. It has to reconcile both ways against the running system: an obligation with no enforcement point and an enforcement point with no declared obligation are each findings. Unreconciled, a register records what you believe you enforce, and the gap from what you actually enforce is invisible in the direction that matters.",
6113
+ kind: "select",
6114
+ options: [
6115
+ "Live register, reconciled both ways against the running system",
6116
+ "A register exists, maintained by hand",
6117
+ "It's spread across CI config, branch rules and instruction files",
6118
+ "No — we would have to go and look",
6119
+ ],
6120
+ via: [
6121
+ "Generated from policy-as-code",
6122
+ "Compliance platform",
6123
+ "In-house control dashboard",
6124
+ "Spreadsheet",
6125
+ "None",
6126
+ ],
6127
+ },
6055
6128
  {
6056
6129
  q: "Is autonomy the same everywhere, or tiered by risk?",
6057
6130
  help: "TASC AC8.8 — organizations running agent-authored code at scale tier the codebase and decide per tier what agents may do unattended, with whole codebases kept under mandatory human approval. Uniform autonomy is a decision nobody made: a tier no one chose is a tier no one is accountable for.",