@intentius/chant 0.52.1 → 0.53.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/agents/checks.d.ts +35 -0
- package/dist/agents/checks.d.ts.map +1 -0
- package/dist/agents/discover.d.ts +86 -0
- package/dist/agents/discover.d.ts.map +1 -0
- package/dist/agents/importer.d.ts +46 -0
- package/dist/agents/importer.d.ts.map +1 -0
- package/dist/agents/index.d.ts +14 -0
- package/dist/agents/index.d.ts.map +1 -0
- package/dist/agents/types.d.ts +196 -0
- package/dist/agents/types.d.ts.map +1 -0
- package/dist/audit/catalog.d.ts +4 -1
- package/dist/audit/catalog.d.ts.map +1 -1
- package/dist/audit/report.d.ts +8 -0
- package/dist/audit/report.d.ts.map +1 -1
- package/dist/audit/rules-doc.d.ts.map +1 -1
- package/dist/cdk/advise.d.ts +29 -0
- package/dist/cdk/advise.d.ts.map +1 -0
- package/dist/cdk/assembly.d.ts +38 -0
- package/dist/cdk/assembly.d.ts.map +1 -0
- package/dist/cdk/graph.d.ts +68 -0
- package/dist/cdk/graph.d.ts.map +1 -0
- package/dist/cdk/tier-map.d.ts +44 -0
- package/dist/cdk/tier-map.d.ts.map +1 -0
- package/dist/cdk/types.d.ts +114 -0
- package/dist/cdk/types.d.ts.map +1 -0
- package/dist/cli/commands/audit-agents.d.ts +83 -0
- package/dist/cli/commands/audit-agents.d.ts.map +1 -0
- package/dist/cli/commands/carve-apply.d.ts.map +1 -1
- package/dist/cli/commands/carve-bridge.d.ts.map +1 -1
- package/dist/cli/commands/carve-emit.d.ts.map +1 -1
- package/dist/cli/commands/carve.d.ts +48 -6
- package/dist/cli/commands/carve.d.ts.map +1 -1
- package/dist/cli/commands/import-agents.d.ts +64 -0
- package/dist/cli/commands/import-agents.d.ts.map +1 -0
- package/dist/cli/handlers/carve-emit.d.ts.map +1 -1
- package/dist/cli/handlers/carve.d.ts +5 -4
- package/dist/cli/handlers/carve.d.ts.map +1 -1
- package/dist/cli/handlers/lifecycle.d.ts +10 -0
- package/dist/cli/handlers/lifecycle.d.ts.map +1 -1
- package/dist/cli/handlers/misc.d.ts.map +1 -1
- package/dist/cli/main.d.ts.map +1 -1
- package/dist/cli/registry.d.ts +15 -0
- package/dist/cli/registry.d.ts.map +1 -1
- package/dist/identity.d.ts +196 -0
- package/dist/identity.d.ts.map +1 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/lexicon.d.ts +52 -0
- package/dist/lexicon.d.ts.map +1 -1
- package/dist/terraform/adopt-state.d.ts +17 -63
- package/dist/terraform/adopt-state.d.ts.map +1 -1
- package/dist/terraform/aws-resources.d.ts +1 -1
- package/dist/terraform/bridge.d.ts.map +1 -1
- package/dist/terraform/carve-provider.d.ts +142 -0
- package/dist/terraform/carve-provider.d.ts.map +1 -0
- package/dist/terraform/carve.d.ts +36 -3
- package/dist/terraform/carve.d.ts.map +1 -1
- package/dist/terraform/emit-source.d.ts +25 -0
- package/dist/terraform/emit-source.d.ts.map +1 -0
- package/dist/terraform/graduate.d.ts +11 -1
- package/dist/terraform/graduate.d.ts.map +1 -1
- package/dist/terraform/providers/aws.d.ts +19 -0
- package/dist/terraform/providers/aws.d.ts.map +1 -0
- package/dist/terraform/providers/gcp.d.ts +41 -0
- package/dist/terraform/providers/gcp.d.ts.map +1 -0
- package/dist/terraform/providers/index.d.ts +15 -0
- package/dist/terraform/providers/index.d.ts.map +1 -0
- package/dist/terraform/providers/kubernetes.d.ts +29 -0
- package/dist/terraform/providers/kubernetes.d.ts.map +1 -0
- package/dist/terraform/score.d.ts +70 -4
- package/dist/terraform/score.d.ts.map +1 -1
- package/dist/terraform/tier-map.d.ts +38 -26
- package/dist/terraform/tier-map.d.ts.map +1 -1
- package/dist/terraform/types.d.ts +6 -0
- package/dist/terraform/types.d.ts.map +1 -1
- package/dist/yaml.d.ts.map +1 -1
- package/package.json +6 -1
- package/src/agents/checks.test.ts +228 -0
- package/src/agents/checks.ts +429 -0
- package/src/agents/discover.test.ts +310 -0
- package/src/agents/discover.ts +939 -0
- package/src/agents/importer.ts +49 -0
- package/src/agents/index.ts +29 -0
- package/src/agents/types.ts +207 -0
- package/src/audit/catalog.ts +90 -1
- package/src/audit/report.ts +9 -1
- package/src/audit/rules-doc.ts +6 -0
- package/src/cdk/__fixtures__/cdk.out/AppStack.template.json +171 -0
- package/src/cdk/__fixtures__/cdk.out/DataStack.template.json +90 -0
- package/src/cdk/__fixtures__/cdk.out/cdk.out +1 -0
- package/src/cdk/__fixtures__/cdk.out/manifest.json +30 -0
- package/src/cdk/__fixtures__/cdk.out/tree.json +201 -0
- package/src/cdk/__fixtures__/cdk.out-dummy/LookupStack.template.json +29 -0
- package/src/cdk/__fixtures__/cdk.out-dummy/manifest.json +26 -0
- package/src/cdk/advise.test.ts +208 -0
- package/src/cdk/advise.ts +44 -0
- package/src/cdk/assembly.ts +133 -0
- package/src/cdk/graph.test.ts +206 -0
- package/src/cdk/graph.ts +525 -0
- package/src/cdk/tier-map.ts +71 -0
- package/src/cdk/types.ts +115 -0
- package/src/cli/commands/audit-agents.test.ts +260 -0
- package/src/cli/commands/audit-agents.ts +387 -0
- package/src/cli/commands/carve-apply.ts +20 -4
- package/src/cli/commands/carve-bridge.test.ts +30 -0
- package/src/cli/commands/carve-bridge.ts +24 -2
- package/src/cli/commands/carve-emit-k8s.test.ts +262 -0
- package/src/cli/commands/carve-emit-provider.test.ts +207 -0
- package/src/cli/commands/carve-emit.test.ts +72 -1
- package/src/cli/commands/carve-emit.ts +55 -28
- package/src/cli/commands/carve.ts +139 -36
- package/src/cli/commands/import-agents.test.ts +208 -0
- package/src/cli/commands/import-agents.ts +196 -0
- package/src/cli/handlers/carve-emit.ts +8 -1
- package/src/cli/handlers/carve.ts +8 -7
- package/src/cli/handlers/lifecycle.test.ts +187 -1
- package/src/cli/handlers/lifecycle.ts +125 -1
- package/src/cli/handlers/misc.ts +111 -0
- package/src/cli/main.ts +29 -5
- package/src/cli/registry.ts +15 -0
- package/src/identity.test.ts +199 -0
- package/src/identity.ts +346 -0
- package/src/index.ts +1 -0
- package/src/lexicon.ts +65 -0
- package/src/terraform/__fixtures__/gcp-estate/main.tf +60 -0
- package/src/terraform/adopt-state.test.ts +131 -0
- package/src/terraform/adopt-state.ts +22 -167
- package/src/terraform/aws-resources.test.ts +55 -16
- package/src/terraform/aws-resources.ts +1 -1
- package/src/terraform/bridge.test.ts +12 -0
- package/src/terraform/bridge.ts +4 -1
- package/src/terraform/carve-provider.test.ts +155 -0
- package/src/terraform/carve-provider.ts +237 -0
- package/src/terraform/carve.test.ts +55 -1
- package/src/terraform/carve.ts +0 -0
- package/src/terraform/emit-source.ts +39 -0
- package/src/terraform/graduate.test.ts +37 -0
- package/src/terraform/graduate.ts +55 -7
- package/src/terraform/graph.ts +3 -3
- package/src/terraform/providers/aws.ts +169 -0
- package/src/terraform/providers/gcp.test.ts +228 -0
- package/src/terraform/providers/gcp.ts +329 -0
- package/src/terraform/providers/index.ts +21 -0
- package/src/terraform/providers/kubernetes.ts +224 -0
- package/src/terraform/score.ts +111 -25
- package/src/terraform/tier-map.ts +55 -90
- package/src/terraform/types.ts +6 -0
- package/src/yaml.test.ts +54 -0
- package/src/yaml.ts +24 -3
package/src/cli/handlers/misc.ts
CHANGED
|
@@ -5,6 +5,9 @@ import { loadChantConfig } from "../../config";
|
|
|
5
5
|
import { resolve } from "path";
|
|
6
6
|
import { auditCommand, printAuditResult, type AuditFormat, type AuditTier, type AuditFailOn } from "../commands/audit";
|
|
7
7
|
import type { ReportTheme } from "../../audit/report-html";
|
|
8
|
+
import { AGENT_RUNTIMES, AGENT_SCOPES, type AgentRuntime, type AgentScope } from "../../agents/types";
|
|
9
|
+
import { registeredProjectRoots } from "../../agents/discover";
|
|
10
|
+
import { homedir } from "os";
|
|
8
11
|
import type { ResourceSelector } from "../../lexicon";
|
|
9
12
|
import { formatError, formatSuccess, formatWarning, formatBold } from "../format";
|
|
10
13
|
import type { CommandContext } from "../registry";
|
|
@@ -23,6 +26,44 @@ const AUDIT_FORMATS: AuditFormat[] = ["stylish", "json", "sarif", "markdown", "h
|
|
|
23
26
|
const AUDIT_TIERS: AuditTier[] = ["merge-worthy", "all"];
|
|
24
27
|
const AUDIT_FAIL_ON: AuditFailOn[] = ["merge-worthy", "warning", "none"];
|
|
25
28
|
|
|
29
|
+
/**
|
|
30
|
+
* Parse `--scope system,user,project`. Returns `undefined` (meaning "all") when
|
|
31
|
+
* the flag is absent, or an Error the caller reports — an unrecognized scope is
|
|
32
|
+
* rejected rather than ignored, since silently scanning more than the user
|
|
33
|
+
* asked for is exactly the surprise this command exists to prevent.
|
|
34
|
+
*/
|
|
35
|
+
function parseScopes(raw: string | undefined): readonly AgentScope[] | undefined | Error {
|
|
36
|
+
if (!raw) return undefined;
|
|
37
|
+
const parts = raw.split(",").map((s) => s.trim()).filter(Boolean);
|
|
38
|
+
const bad = parts.filter((p) => !AGENT_SCOPES.includes(p as AgentScope));
|
|
39
|
+
if (bad.length > 0) return new Error(`Invalid --scope: ${bad.join(", ")}. Expected one or more of ${AGENT_SCOPES.join(", ")}.`);
|
|
40
|
+
return parts.length > 0 ? (parts as AgentScope[]) : undefined;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/** Parse `--runtime claude,codex,...`. Same contract as {@link parseScopes}. */
|
|
44
|
+
function parseRuntimes(raw: string | undefined): readonly AgentRuntime[] | undefined | Error {
|
|
45
|
+
if (!raw) return undefined;
|
|
46
|
+
const parts = raw.split(",").map((s) => s.trim()).filter(Boolean);
|
|
47
|
+
const bad = parts.filter((p) => !AGENT_RUNTIMES.includes(p as AgentRuntime));
|
|
48
|
+
if (bad.length > 0) return new Error(`Invalid --runtime: ${bad.join(", ")}. Expected one or more of ${AGENT_RUNTIMES.join(", ")}.`);
|
|
49
|
+
return parts.length > 0 ? (parts as AgentRuntime[]) : undefined;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Project roots for an `--agents` run.
|
|
54
|
+
*
|
|
55
|
+
* Default: the path argument alone. With `--all-projects`, every project root
|
|
56
|
+
* the harness has registered, *unioned* with the path argument rather than
|
|
57
|
+
* replacing it — a flag that silently discarded an argument the user typed
|
|
58
|
+
* would be the wrong kind of surprise, and a cwd that isn't registered yet is
|
|
59
|
+
* still a project the user is plainly interested in.
|
|
60
|
+
*/
|
|
61
|
+
function resolveProjectRoots(args: { path: string; allProjects?: boolean }): string[] {
|
|
62
|
+
const named = resolve(args.path);
|
|
63
|
+
if (!args.allProjects) return [named];
|
|
64
|
+
return [...new Set([...registeredProjectRoots(homedir()), named])].sort();
|
|
65
|
+
}
|
|
66
|
+
|
|
26
67
|
export async function runAudit(ctx: CommandContext): Promise<number> {
|
|
27
68
|
const { args } = ctx;
|
|
28
69
|
|
|
@@ -42,6 +83,37 @@ export async function runAudit(ctx: CommandContext): Promise<number> {
|
|
|
42
83
|
return 1;
|
|
43
84
|
}
|
|
44
85
|
|
|
86
|
+
// `--agents` switches the subject from a repo to this machine's agent
|
|
87
|
+
// configuration. Same formats, tiers, and fail-on policy — a different thing
|
|
88
|
+
// being audited, not a different report.
|
|
89
|
+
if (args.agents) {
|
|
90
|
+
const scopes = parseScopes(args.scope);
|
|
91
|
+
if (scopes instanceof Error) {
|
|
92
|
+
console.error(formatError({ message: scopes.message }));
|
|
93
|
+
return 1;
|
|
94
|
+
}
|
|
95
|
+
const runtimes = parseRuntimes(args.runtime);
|
|
96
|
+
if (runtimes instanceof Error) {
|
|
97
|
+
console.error(formatError({ message: runtimes.message }));
|
|
98
|
+
return 1;
|
|
99
|
+
}
|
|
100
|
+
const { auditAgentsCommand } = await import("../commands/audit-agents");
|
|
101
|
+
const result = auditAgentsCommand({
|
|
102
|
+
format,
|
|
103
|
+
tier,
|
|
104
|
+
failOn,
|
|
105
|
+
scopes,
|
|
106
|
+
runtimes,
|
|
107
|
+
projectRoots: resolveProjectRoots(args),
|
|
108
|
+
output: args.output,
|
|
109
|
+
toolVersion: CHANT_VERSION,
|
|
110
|
+
});
|
|
111
|
+
if (result.error) console.error(formatError({ message: result.error }));
|
|
112
|
+
else if (result.wroteTo) console.log(formatSuccess(`Report written to ${result.wroteTo}`));
|
|
113
|
+
else console.log(result.output);
|
|
114
|
+
return result.exitCode;
|
|
115
|
+
}
|
|
116
|
+
|
|
45
117
|
// HTML report customization: --template <file> (full override) + --theme <file> (JSON knobs).
|
|
46
118
|
let template: string | undefined;
|
|
47
119
|
let theme: ReportTheme | undefined;
|
|
@@ -171,6 +243,45 @@ export async function runDescribe(ctx: CommandContext): Promise<number> {
|
|
|
171
243
|
export async function runImport(ctx: CommandContext): Promise<number> {
|
|
172
244
|
const { args } = ctx;
|
|
173
245
|
|
|
246
|
+
// `--agents` (#chant audit --agents' other half): the source is this
|
|
247
|
+
// machine's agent configuration rather than a template file, so there is
|
|
248
|
+
// nothing to detect — the flag names the subject, like `--kustomize` below
|
|
249
|
+
// names the lexicon.
|
|
250
|
+
if (args.agents) {
|
|
251
|
+
const scopes = parseScopes(args.scope);
|
|
252
|
+
if (scopes instanceof Error) {
|
|
253
|
+
console.error(formatError({ message: scopes.message }));
|
|
254
|
+
return 1;
|
|
255
|
+
}
|
|
256
|
+
const runtimes = parseRuntimes(args.runtime);
|
|
257
|
+
if (runtimes instanceof Error) {
|
|
258
|
+
console.error(formatError({ message: runtimes.message }));
|
|
259
|
+
return 1;
|
|
260
|
+
}
|
|
261
|
+
const { importAgentsCommand } = await import("../commands/import-agents");
|
|
262
|
+
const result = await importAgentsCommand({
|
|
263
|
+
lexicon: args.lexicon,
|
|
264
|
+
output: args.output,
|
|
265
|
+
force: args.force,
|
|
266
|
+
scopes,
|
|
267
|
+
runtimes,
|
|
268
|
+
projectRoots: resolveProjectRoots(args),
|
|
269
|
+
});
|
|
270
|
+
for (const warning of result.warnings) console.warn(formatWarning({ message: warning }));
|
|
271
|
+
if (!result.success) {
|
|
272
|
+
console.error(formatError({ message: result.error ?? "Import failed." }));
|
|
273
|
+
return 1;
|
|
274
|
+
}
|
|
275
|
+
console.log(
|
|
276
|
+
formatSuccess(
|
|
277
|
+
`Re-expressed ${result.summary.mapped} resource${result.summary.mapped === 1 ? "" : "s"} ` +
|
|
278
|
+
`from ${result.summary.discovered} agent config${result.summary.discovered === 1 ? "" : "s"}:`,
|
|
279
|
+
),
|
|
280
|
+
);
|
|
281
|
+
for (const file of result.generatedFiles) console.log(` ${file}`);
|
|
282
|
+
return 0;
|
|
283
|
+
}
|
|
284
|
+
|
|
174
285
|
// `--kustomize <dir>` (#1548): render the overlay and import the output
|
|
175
286
|
// through the k8s template parser — the flag NAMES the lexicon, so no JSON
|
|
176
287
|
// detection. `kustomize build`, falling back to kubectl's vendored
|
package/src/cli/main.ts
CHANGED
|
@@ -23,7 +23,7 @@ import { runCarveAdvise, runCarveUnknown } from "./handlers/carve";
|
|
|
23
23
|
import { runCarveEmit } from "./handlers/carve-emit";
|
|
24
24
|
import { runCarveBridge } from "./handlers/carve-bridge";
|
|
25
25
|
import { runCarveApply } from "./handlers/carve-apply";
|
|
26
|
-
import { runLifecycleSnapshot, runLifecycleShow, runLifecycleDiff, runLifecycleRollback, runLifecyclePlan, runLifecycleAffected, runLifecycleLog, runLifecycleTeardown, runLifecycleUnknown } from "./handlers/lifecycle";
|
|
26
|
+
import { runLifecycleSnapshot, runLifecycleShow, runLifecycleDiff, runLifecycleRollback, runLifecyclePlan, runLifecycleAffected, runLifecycleLog, runLifecycleTeardown, runLifecycleWhoami, runLifecycleUnknown } from "./handlers/lifecycle";
|
|
27
27
|
import { runComponentsStatus, runComponentsReleaseRecord, runComponentsExport, runComponentsUnknown } from "./handlers/components";
|
|
28
28
|
import { runScenarioCheck, runScenarioUnknown } from "./handlers/scenario";
|
|
29
29
|
import { runGraph } from "./handlers/graph";
|
|
@@ -48,6 +48,8 @@ import type { LexiconPlugin } from "../lexicon";
|
|
|
48
48
|
*/
|
|
49
49
|
const BOOLEAN_FLAGS = new Set([
|
|
50
50
|
"--help",
|
|
51
|
+
"--agents",
|
|
52
|
+
"--all-projects",
|
|
51
53
|
"--force",
|
|
52
54
|
"--fix",
|
|
53
55
|
"--watch",
|
|
@@ -254,6 +256,14 @@ export function parseArgs(args: string[]): ParsedArgs {
|
|
|
254
256
|
result.env = args[++i];
|
|
255
257
|
} else if (arg === "--tier") {
|
|
256
258
|
result.tier = args[++i];
|
|
259
|
+
} else if (arg === "--agents") {
|
|
260
|
+
result.agents = true;
|
|
261
|
+
} else if (arg === "--scope") {
|
|
262
|
+
result.scope = args[++i];
|
|
263
|
+
} else if (arg === "--all-projects") {
|
|
264
|
+
result.allProjects = true;
|
|
265
|
+
} else if (arg === "--runtime") {
|
|
266
|
+
result.runtime = args[++i];
|
|
257
267
|
} else if (arg === "--fail-on") {
|
|
258
268
|
result.failOn = args[++i];
|
|
259
269
|
} else if (arg === "--theme") {
|
|
@@ -438,16 +448,25 @@ Commands:
|
|
|
438
448
|
bundle tree, otherwise JSON path→content on stdout)
|
|
439
449
|
vendor Pull pinned, checksummed patterns into your repo
|
|
440
450
|
import Import external template into TypeScript
|
|
451
|
+
(--agents re-expresses this machine's agent config as
|
|
452
|
+
chant code instead of reading a template file)
|
|
441
453
|
audit [path|url] Audit a repo's CI YAML for security issues
|
|
442
454
|
(--format stylish|json|sarif|markdown|html, -o <file>,
|
|
443
455
|
--tier merge-worthy|all, --fail-on merge-worthy|warning|none,
|
|
444
456
|
--template <file> / --theme <file> for the html report)
|
|
457
|
+
--agents audits this machine's agent configuration —
|
|
458
|
+
instruction files, MCP servers, skills, plugins,
|
|
459
|
+
permissions — instead of a repository
|
|
460
|
+
(--scope system,user,project, --runtime claude,codex,...,
|
|
461
|
+
--all-projects)
|
|
445
462
|
migrate <file> Translate a workflow between lexicons
|
|
446
463
|
(default: --from github --to gitlab)
|
|
447
|
-
carve advise Read-only
|
|
448
|
-
--from <
|
|
464
|
+
carve advise Read-only peelability advisor: rank which resources
|
|
465
|
+
--from <dir> are cheap to carve into native chant
|
|
449
466
|
(--json, --report <path>). Emits nothing, changes nothing.
|
|
450
|
-
|
|
467
|
+
--from a Terraform dir needs @cdktf/hcl2json
|
|
468
|
+
(npm install -D @cdktf/hcl2json); --from a CDK cloud
|
|
469
|
+
assembly (cdk.out) needs nothing and ranks constructs.
|
|
451
470
|
carve emit Adopt a selected TF resource into chant source + report
|
|
452
471
|
--from <tf-dir> its boundary. --state <tfstate> adopts offline
|
|
453
472
|
--select <addr> (recommended for TF-managed resources); --env
|
|
@@ -539,6 +558,9 @@ Lifecycle (alias: lc):
|
|
|
539
558
|
lifecycle plan <env> Typed change set (create/update/delete/adopt) vs live
|
|
540
559
|
lifecycle affected Stacks a change affects (--base <ref> [--include-dependents])
|
|
541
560
|
--json: emit the ChangeSet as JSON
|
|
561
|
+
lifecycle whoami <env> Who chant would act as in each configured lexicon,
|
|
562
|
+
and what that principal is scoped to — read-only,
|
|
563
|
+
before anything acts (--json, --strict)
|
|
542
564
|
lifecycle teardown <env> Plan what deleting the environment would remove —
|
|
543
565
|
marker-scoped (this project's stack + env); --yes
|
|
544
566
|
executes the plan (production-like names also need
|
|
@@ -629,7 +651,8 @@ Options:
|
|
|
629
651
|
--from <name> Source lexicon for migrate (default: github)
|
|
630
652
|
--to <name> Target lexicon for migrate (default: gitlab)
|
|
631
653
|
--emit <fmt> Migration output format: yaml (default) or ts
|
|
632
|
-
--strict Escalate needs-review/validation to errors (migrate)
|
|
654
|
+
--strict Escalate needs-review/validation to errors (migrate);
|
|
655
|
+
exit nonzero on an unresolved identity (lifecycle whoami)
|
|
633
656
|
--validate Run external validator (glci/glab) after migrate
|
|
634
657
|
--use-composites Rewrite to composite calls when patterns match (migrate)
|
|
635
658
|
--components Target discovered Component declarations instead of
|
|
@@ -865,6 +888,7 @@ const registry: CommandDef[] = [
|
|
|
865
888
|
{ name: "lifecycle rollback", handler: runLifecycleRollback },
|
|
866
889
|
{ name: "lifecycle plan", requiresPlugins: true, handler: runLifecyclePlan },
|
|
867
890
|
{ name: "lifecycle affected", requiresPlugins: true, handler: runLifecycleAffected },
|
|
891
|
+
{ name: "lifecycle whoami", requiresPlugins: true, handler: runLifecycleWhoami },
|
|
868
892
|
{ name: "lifecycle teardown", requiresPlugins: true, handler: runLifecycleTeardown },
|
|
869
893
|
{ name: "lifecycle log", handler: runLifecycleLog },
|
|
870
894
|
|
package/src/cli/registry.ts
CHANGED
|
@@ -158,6 +158,21 @@ export interface ParsedArgs {
|
|
|
158
158
|
includeDependents?: boolean;
|
|
159
159
|
/** `chant audit --tier merge-worthy|all` */
|
|
160
160
|
tier?: string;
|
|
161
|
+
/**
|
|
162
|
+
* `chant audit --agents` / `chant import --agents` — switch the subject from
|
|
163
|
+
* a repo's pipeline YAML to the agent configuration installed on this
|
|
164
|
+
* machine (skills, MCP servers, instruction files, permissions).
|
|
165
|
+
*/
|
|
166
|
+
agents?: boolean;
|
|
167
|
+
/** `--agents --scope system,user,project` — which scopes to scan (default all). */
|
|
168
|
+
scope?: string;
|
|
169
|
+
/** `--agents --runtime claude,codex,...` — which agent harnesses to scan (default all). */
|
|
170
|
+
runtime?: string;
|
|
171
|
+
/**
|
|
172
|
+
* `--agents --all-projects` — scan every project root the harness has
|
|
173
|
+
* registered in `~/.claude.json`, not just the path argument.
|
|
174
|
+
*/
|
|
175
|
+
allProjects?: boolean;
|
|
161
176
|
/** `chant audit --fail-on merge-worthy|warning|none` */
|
|
162
177
|
failOn?: string;
|
|
163
178
|
/** `chant audit --theme <file>` — JSON theme knobs for the HTML report */
|
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
import { describe, test, expect } from "vitest";
|
|
2
|
+
import {
|
|
3
|
+
describeIdentities,
|
|
4
|
+
identityRowFor,
|
|
5
|
+
identityStatusText,
|
|
6
|
+
isUnresolvedIdentity,
|
|
7
|
+
redactCredentialMaterial,
|
|
8
|
+
redactIdentityRow,
|
|
9
|
+
REDACTED,
|
|
10
|
+
type IdentityPlugin,
|
|
11
|
+
} from "./identity";
|
|
12
|
+
|
|
13
|
+
describe("the identity tri-state (#1982)", () => {
|
|
14
|
+
test("a lexicon with no describeIdentity is not-reported, never an empty identity", async () => {
|
|
15
|
+
const rows = await describeIdentities([{ name: "github" }], { environment: "prod" }, {});
|
|
16
|
+
expect(rows).toEqual([{ lexicon: "github", status: "not-reported" }]);
|
|
17
|
+
expect(rows[0].identity).toBeUndefined();
|
|
18
|
+
expect(identityStatusText(rows[0])).toContain("not reported");
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
test("a resolved identity carries principal, scope, source and endpoint", async () => {
|
|
22
|
+
const plugin: IdentityPlugin = {
|
|
23
|
+
name: "aws",
|
|
24
|
+
describeIdentity: async () => ({
|
|
25
|
+
identity: "arn:aws:sts::491500000000:assumed-role/deploy/ci",
|
|
26
|
+
scope: "491500000000 us-east-1",
|
|
27
|
+
source: "env AWS_ACCESS_KEY_ID",
|
|
28
|
+
endpoint: "https://sts.us-east-1.amazonaws.com/",
|
|
29
|
+
}),
|
|
30
|
+
};
|
|
31
|
+
const [row] = await describeIdentities([plugin], { environment: "prod" }, {});
|
|
32
|
+
expect(row).toEqual({
|
|
33
|
+
lexicon: "aws",
|
|
34
|
+
status: "reported",
|
|
35
|
+
identity: "arn:aws:sts::491500000000:assumed-role/deploy/ci",
|
|
36
|
+
scope: "491500000000 us-east-1",
|
|
37
|
+
source: "env AWS_ACCESS_KEY_ID",
|
|
38
|
+
endpoint: "https://sts.us-east-1.amazonaws.com/",
|
|
39
|
+
});
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
test("no credentials and could-not-determine are distinguishable answers", async () => {
|
|
43
|
+
const rows = await describeIdentities(
|
|
44
|
+
[
|
|
45
|
+
{ name: "aws", describeIdentity: async () => ({ unresolved: { reason: "no-credentials" } }) },
|
|
46
|
+
{
|
|
47
|
+
name: "k8s",
|
|
48
|
+
describeIdentity: async () => ({ unresolved: { reason: "read-failed", detail: "connection refused" } }),
|
|
49
|
+
},
|
|
50
|
+
],
|
|
51
|
+
{ environment: "prod" },
|
|
52
|
+
{},
|
|
53
|
+
);
|
|
54
|
+
expect(rows[0]).toEqual({ lexicon: "aws", status: "unresolved", reason: "no-credentials" });
|
|
55
|
+
expect(identityStatusText(rows[0])).toContain("no identity is configured");
|
|
56
|
+
expect(rows[1]).toEqual({
|
|
57
|
+
lexicon: "k8s",
|
|
58
|
+
status: "unresolved",
|
|
59
|
+
reason: "read-failed",
|
|
60
|
+
detail: "connection refused",
|
|
61
|
+
});
|
|
62
|
+
expect(identityStatusText(rows[1])).toContain("self-query failed");
|
|
63
|
+
// The point of the distinction: "nothing is configured" and "I could not
|
|
64
|
+
// find out" must never render as the same row.
|
|
65
|
+
expect(identityStatusText(rows[0])).not.toBe(identityStatusText(rows[1]));
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
test("an empty principal is refused rather than reported as an identity", () => {
|
|
69
|
+
const row = identityRowFor("gcp", { identity: " ", scope: "acme-prod", source: "ADC" }, {});
|
|
70
|
+
expect(row.status).toBe("unresolved");
|
|
71
|
+
expect(row.reason).toBe("read-failed");
|
|
72
|
+
expect(row.identity).toBeUndefined();
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
test("a lexicon that throws degrades to read-failed and never fails the run", async () => {
|
|
76
|
+
const rows = await describeIdentities(
|
|
77
|
+
[
|
|
78
|
+
{
|
|
79
|
+
name: "azure",
|
|
80
|
+
describeIdentity: async () => {
|
|
81
|
+
throw new Error("az CLI is not installed");
|
|
82
|
+
},
|
|
83
|
+
},
|
|
84
|
+
{ name: "aws", describeIdentity: async () => ({ identity: "arn:x", scope: "s", source: "env" }) },
|
|
85
|
+
],
|
|
86
|
+
{ environment: "prod" },
|
|
87
|
+
{},
|
|
88
|
+
);
|
|
89
|
+
expect(rows[0]).toEqual({
|
|
90
|
+
lexicon: "azure",
|
|
91
|
+
status: "unresolved",
|
|
92
|
+
reason: "read-failed",
|
|
93
|
+
detail: "az CLI is not installed",
|
|
94
|
+
});
|
|
95
|
+
expect(rows[1].status).toBe("reported");
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
test("rows come back in the order the lexicons were configured", async () => {
|
|
99
|
+
const names = ["k8s", "aws", "gcp", "github"];
|
|
100
|
+
const rows = await describeIdentities(
|
|
101
|
+
names.map((name) => ({ name })),
|
|
102
|
+
{ environment: "prod" },
|
|
103
|
+
{},
|
|
104
|
+
);
|
|
105
|
+
expect(rows.map((r) => r.lexicon)).toEqual(names);
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
test("the environment, region and cwd reach the lexicon unchanged", async () => {
|
|
109
|
+
const seen: unknown[] = [];
|
|
110
|
+
await describeIdentities(
|
|
111
|
+
[
|
|
112
|
+
{
|
|
113
|
+
name: "aws",
|
|
114
|
+
describeIdentity: async (options) => {
|
|
115
|
+
seen.push(options);
|
|
116
|
+
return { identity: "arn:x", scope: "s", source: "env" };
|
|
117
|
+
},
|
|
118
|
+
},
|
|
119
|
+
],
|
|
120
|
+
{ environment: "prod", region: "eu-west-1", cwd: "/repo" },
|
|
121
|
+
{},
|
|
122
|
+
);
|
|
123
|
+
expect(seen).toEqual([{ environment: "prod", region: "eu-west-1", cwd: "/repo" }]);
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
test("isUnresolvedIdentity discriminates the two return shapes", () => {
|
|
127
|
+
expect(isUnresolvedIdentity({ unresolved: { reason: "no-binding" } })).toBe(true);
|
|
128
|
+
expect(isUnresolvedIdentity({ identity: "a", scope: "b", source: "c" })).toBe(false);
|
|
129
|
+
});
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
describe("no credential reaches the output (#1982)", () => {
|
|
133
|
+
test("a value held in a credential-named env var is redacted wherever it appears", () => {
|
|
134
|
+
const env = { GITHUB_TOKEN: "ghp_averyrealtokenvalue", HOME: "/Users/x" };
|
|
135
|
+
expect(redactCredentialMaterial("authorized by ghp_averyrealtokenvalue", env)).toBe(
|
|
136
|
+
`authorized by ${REDACTED}`,
|
|
137
|
+
);
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
test("every free-text field of a row is scrubbed, not just the identity", () => {
|
|
141
|
+
const env = { AWS_SECRET_ACCESS_KEY: "wJalrXUtnFEMIsecretKEY" };
|
|
142
|
+
const row = redactIdentityRow(
|
|
143
|
+
{
|
|
144
|
+
lexicon: "aws",
|
|
145
|
+
status: "reported",
|
|
146
|
+
identity: "wJalrXUtnFEMIsecretKEY",
|
|
147
|
+
scope: "wJalrXUtnFEMIsecretKEY",
|
|
148
|
+
source: "signed with wJalrXUtnFEMIsecretKEY",
|
|
149
|
+
endpoint: "https://sts.amazonaws.com/?k=wJalrXUtnFEMIsecretKEY",
|
|
150
|
+
detail: "wJalrXUtnFEMIsecretKEY",
|
|
151
|
+
},
|
|
152
|
+
env,
|
|
153
|
+
);
|
|
154
|
+
for (const field of [row.identity, row.scope, row.source, row.endpoint, row.detail]) {
|
|
155
|
+
expect(field).not.toContain("wJalrXUtnFEMIsecretKEY");
|
|
156
|
+
expect(field).toContain(REDACTED);
|
|
157
|
+
}
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
test("a short env value is not treated as a credential", () => {
|
|
161
|
+
// Redacting "dev" out of every identity would be worse than the leak.
|
|
162
|
+
expect(redactCredentialMaterial("cluster dev-a", { AUTH_TOKEN: "dev" })).toBe("cluster dev-a");
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
test("literal credential shapes are redacted with no env var involved", () => {
|
|
166
|
+
const jwt = "eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiIxIn0.dBjftJeZ4CVPmB92K27u";
|
|
167
|
+
expect(redactCredentialMaterial(jwt, {})).toBe(REDACTED);
|
|
168
|
+
expect(redactCredentialMaterial("Bearer abcdefghijklmnopqrstuvwxyz012345", {})).toBe(REDACTED);
|
|
169
|
+
expect(
|
|
170
|
+
redactCredentialMaterial("-----BEGIN RSA PRIVATE KEY-----\nAAAA\n-----END RSA PRIVATE KEY-----", {}),
|
|
171
|
+
).toBe(REDACTED);
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
test("real principals survive redaction unchanged", () => {
|
|
175
|
+
// A mangled principal is a wrong answer, not a safe one. Nothing here is
|
|
176
|
+
// entropy-scored, so these pass through whole.
|
|
177
|
+
const env = { AWS_SESSION_TOKEN: "FwoGZXIvYXdzEBYaDLONGSESSIONTOKEN" };
|
|
178
|
+
for (const principal of [
|
|
179
|
+
"arn:aws:sts::491500000000:assumed-role/deploy/ci",
|
|
180
|
+
"system:serviceaccount:chant:deployer",
|
|
181
|
+
"deploy@acme.iam.gserviceaccount.com",
|
|
182
|
+
"https://prod-eks-a.eu-west-1.eks.amazonaws.com",
|
|
183
|
+
"491500000000 us-east-1",
|
|
184
|
+
]) {
|
|
185
|
+
expect(redactCredentialMaterial(principal, env)).toBe(principal);
|
|
186
|
+
}
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
test("an identity built out of the session token is redacted, not printed", () => {
|
|
190
|
+
const env = { AWS_SESSION_TOKEN: "FwoGZXIvYXdzEBYaDLONGSESSIONTOKEN" };
|
|
191
|
+
const row = identityRowFor(
|
|
192
|
+
"aws",
|
|
193
|
+
{ identity: "FwoGZXIvYXdzEBYaDLONGSESSIONTOKEN", scope: "us-east-1", source: "env" },
|
|
194
|
+
env,
|
|
195
|
+
);
|
|
196
|
+
expect(row.identity).toBe(REDACTED);
|
|
197
|
+
expect(row.identity).not.toContain("FwoGZXIvYXdz");
|
|
198
|
+
});
|
|
199
|
+
});
|