@mnstry/atelier 0.2.0-alpha.4 → 0.2.0-alpha.5
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 +62 -0
- package/README.md +38 -12
- package/contracts/public-api-baseline.json +57 -0
- package/docs/assurance-controls.md +39 -0
- package/docs/atelier-runtime.md +15 -0
- package/docs/blocks/claims.md +15 -9
- package/docs/design.md +12 -6
- package/docs/install.md +26 -4
- package/docs/knowledge-graph.md +8 -4
- package/docs/local-services.md +101 -0
- package/docs/release-engineering.md +75 -10
- package/docs/repo-boundary-guard.md +12 -2
- package/docs/upgrade.md +25 -2
- package/fixtures/projects/sample-workspace/content/source.html.kg.json +4 -1
- package/fixtures/projects/source-formats-workspace/content/data.json.kg.json +4 -1
- package/fixtures/projects/source-formats-workspace/content/logo.png.kg.json +4 -1
- package/fixtures/projects/source-formats-workspace/content/metrics.csv.kg.json +4 -1
- package/fixtures/projects/source-formats-workspace/content/pipeline.yaml.kg.json +4 -1
- package/package.json +12 -5
- package/skills/claude/atelier-local-service/SKILL.md +47 -0
- package/skills/claude/atelier-public-boundary/SKILL.md +31 -0
- package/skills/codex/atelier-local-service/SKILL.md +47 -0
- package/skills/codex/atelier-public-boundary/SKILL.md +31 -0
- package/src/boundary/content-rules.mjs +278 -20
- package/src/boundary/policy.mjs +150 -60
- package/src/cli/execute-command.mjs +36 -0
- package/src/cli/run.mjs +17 -7
- package/src/collaboration/event-ledger.mjs +365 -0
- package/src/collaboration/index.mjs +17 -0
- package/src/collaboration/proposals.mjs +265 -65
- package/src/commands/attestation.mjs +20 -6
- package/src/commands/disclosure.mjs +133 -0
- package/src/commands/distribution.mjs +2 -1
- package/src/commands/extension-pack.mjs +2 -1
- package/src/commands/init.mjs +2 -1
- package/src/commands/server.mjs +1 -4
- package/src/disclosure/content-scan.mjs +193 -0
- package/src/egress/check.mjs +7 -38
- package/src/egress/forbidden-egress.mjs +32 -18
- package/src/graph/graph.mjs +112 -314
- package/src/graph/knowledge-graph.mjs +94 -18
- package/src/harness/context-client.mjs +9 -1
- package/src/index.mjs +12 -0
- package/src/project/config.mjs +66 -7
- package/src/project/file-class.mjs +14 -0
- package/src/project/package-root.mjs +10 -0
- package/src/project/path-match.mjs +38 -15
- package/src/project/private-state.mjs +110 -0
- package/src/server/local-sidecar.mjs +81 -59
- package/src/server/security.mjs +89 -4
- package/src/server/server.mjs +3 -2
- package/src/support/feedback-report.mjs +4 -3
- package/src/upgrade/upgrade.mjs +2 -1
package/src/boundary/policy.mjs
CHANGED
|
@@ -5,13 +5,12 @@ import { buildGraph } from '../graph/graph.mjs'
|
|
|
5
5
|
import { commandProject, firstString, parseArgs, readJson, resolvePathValue, writeJson } from '../project/config.mjs'
|
|
6
6
|
import { matchesPathPattern } from '../project/path-match.mjs'
|
|
7
7
|
import {
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
parsePushRefUpdates,
|
|
8
|
+
CHECK_AGGREGATE_MAX_BYTES,
|
|
9
|
+
parsePushRefInput,
|
|
11
10
|
resolveContentRules,
|
|
12
|
-
|
|
11
|
+
scanPushUpdate,
|
|
12
|
+
scanStagedRepository,
|
|
13
13
|
scanTree,
|
|
14
|
-
stagedDiff,
|
|
15
14
|
validateContentRuleExceptions,
|
|
16
15
|
validateContentRules,
|
|
17
16
|
} from './content-rules.mjs'
|
|
@@ -230,8 +229,13 @@ function gitEmailsForProject(project) {
|
|
|
230
229
|
const emails = []
|
|
231
230
|
for (const root of roots) {
|
|
232
231
|
if (!root || !fs.existsSync(root)) continue
|
|
233
|
-
const
|
|
234
|
-
|
|
232
|
+
for (const args of [
|
|
233
|
+
['config', 'user.email'],
|
|
234
|
+
['log', '-1', '--format=%ae'],
|
|
235
|
+
]) {
|
|
236
|
+
const result = spawnSync('git', ['-C', root, ...args], { encoding: 'utf8' })
|
|
237
|
+
if (result.status === 0 && result.stdout.trim()) emails.push(result.stdout.trim())
|
|
238
|
+
}
|
|
235
239
|
}
|
|
236
240
|
return unique(emails)
|
|
237
241
|
}
|
|
@@ -308,7 +312,7 @@ export function stagedPathsForProject(project) {
|
|
|
308
312
|
function forbiddenPathFindings({ policy, stagedPaths }) {
|
|
309
313
|
const patterns = [...DEFAULT_FORBIDDEN_PATHS, ...asArray(policy.forbiddenPaths)]
|
|
310
314
|
const findings = []
|
|
311
|
-
const severity =
|
|
315
|
+
const severity = 'error'
|
|
312
316
|
for (const item of stagedPaths) {
|
|
313
317
|
const matched = patterns.find((pattern) => matchesPathPattern(pattern, item.path))
|
|
314
318
|
if (matched) {
|
|
@@ -392,11 +396,18 @@ export function semanticChangesInFile(file) {
|
|
|
392
396
|
|
|
393
397
|
function semanticDiffFindings({ policy, project }) {
|
|
394
398
|
const findings = []
|
|
395
|
-
const severity =
|
|
399
|
+
const severity = 'error'
|
|
396
400
|
for (const repo of managedRepos(project)) {
|
|
397
|
-
if (!repo.path || !fs.existsSync(path.join(repo.path, '.git')))
|
|
401
|
+
if (!repo.path || !fs.existsSync(path.join(repo.path, '.git'))) {
|
|
402
|
+
findings.push(unscannableRepoFinding(repo, 'configured path is absent or is not a Git checkout'))
|
|
403
|
+
continue
|
|
404
|
+
}
|
|
398
405
|
const result = spawnSync('git', ['-C', repo.path, 'diff', '--cached', '--unified=0', '--', '*.md', '*.kg.json'], { encoding: 'utf8' })
|
|
399
|
-
if (result.status !== 0
|
|
406
|
+
if (result.status !== 0) {
|
|
407
|
+
findings.push(unscannableRepoFinding(repo, 'git diff --cached failed'))
|
|
408
|
+
continue
|
|
409
|
+
}
|
|
410
|
+
if (!result.stdout.trim()) continue
|
|
400
411
|
for (const file of diffFileSections(result.stdout)) {
|
|
401
412
|
const changes = semanticChangesInFile(file)
|
|
402
413
|
if (!changes.length) continue
|
|
@@ -419,27 +430,32 @@ function semanticDiffFindings({ policy, project }) {
|
|
|
419
430
|
return findings
|
|
420
431
|
}
|
|
421
432
|
|
|
422
|
-
function
|
|
433
|
+
function stagedContentFindings({ policy, project }) {
|
|
423
434
|
const rules = resolveContentRules(policy)
|
|
424
435
|
const exceptions = asArray(policy?.contentRuleExceptions)
|
|
425
436
|
const findings = []
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
437
|
+
let totalBytes = 0
|
|
438
|
+
for (const repo of managedRepos(project)) {
|
|
439
|
+
if (!repo.path || !fs.existsSync(path.join(repo.path, '.git'))) {
|
|
440
|
+
findings.push(unscannableRepoFinding(repo, 'configured path is absent or is not a Git checkout'))
|
|
441
|
+
continue
|
|
442
|
+
}
|
|
443
|
+
const result = scanStagedRepository({ repoRoot: repo.path, rules, exceptions, repo: repo.name })
|
|
444
|
+
findings.push(...result.findings, ...result.diagnostics)
|
|
445
|
+
totalBytes += result.bytes
|
|
446
|
+
if (totalBytes > CHECK_AGGREGATE_MAX_BYTES) {
|
|
447
|
+
findings.push(finding({
|
|
448
|
+
severity: 'error',
|
|
449
|
+
code: 'content-scan-incomplete',
|
|
450
|
+
repo: repo.name,
|
|
451
|
+
message: `staged evidence exceeds the ${CHECK_AGGREGATE_MAX_BYTES}-byte aggregate limit`,
|
|
452
|
+
}))
|
|
453
|
+
break
|
|
431
454
|
}
|
|
432
455
|
}
|
|
433
456
|
return findings
|
|
434
457
|
}
|
|
435
458
|
|
|
436
|
-
function stagedContentFindings({ policy, project }) {
|
|
437
|
-
const files = managedRepos(project)
|
|
438
|
-
.filter((repo) => repo.path && fs.existsSync(path.join(repo.path, '.git')))
|
|
439
|
-
.map((repo) => ({ repo: repo.name, files: parseAddedContent(stagedDiff(repo.path)) }))
|
|
440
|
-
return contentRuleFindings({ policy, project, files })
|
|
441
|
-
}
|
|
442
|
-
|
|
443
459
|
function realPath(value) {
|
|
444
460
|
try {
|
|
445
461
|
return fs.realpathSync(path.resolve(value))
|
|
@@ -467,7 +483,7 @@ function repoForCwd(project, cwd) {
|
|
|
467
483
|
* which reports without blocking, so an accepted usage elsewhere in the repo can
|
|
468
484
|
* never strand unrelated work on the machine.
|
|
469
485
|
*/
|
|
470
|
-
export function checkPushContent({ project, policy, repo, updates, cwd = process.cwd() } = {}) {
|
|
486
|
+
export function checkPushContent({ project, policy, repo, updates, cwd = process.cwd(), gitRunner = spawnSync } = {}) {
|
|
471
487
|
const target = repo ?? repoForCwd(project, cwd)
|
|
472
488
|
if (!target) {
|
|
473
489
|
// Fail closed. This hook is only installed into managed repos, so failing to
|
|
@@ -480,20 +496,41 @@ export function checkPushContent({ project, policy, repo, updates, cwd = process
|
|
|
480
496
|
})
|
|
481
497
|
return { ok: false, repo: null, findings: [unresolved], errors: [unresolved], warnings: [] }
|
|
482
498
|
}
|
|
483
|
-
const
|
|
484
|
-
|
|
485
|
-
const findings =
|
|
499
|
+
const rules = resolveContentRules(policy)
|
|
500
|
+
const exceptions = asArray(policy?.contentRuleExceptions)
|
|
501
|
+
const findings = []
|
|
502
|
+
let totalBytes = 0
|
|
503
|
+
for (const update of updates) {
|
|
504
|
+
const result = scanPushUpdate({ repoRoot: target.path, update, rules, exceptions, repo: target.name, gitRunner })
|
|
505
|
+
findings.push(...result.findings, ...result.diagnostics)
|
|
506
|
+
totalBytes += result.bytes
|
|
507
|
+
if (totalBytes > CHECK_AGGREGATE_MAX_BYTES) {
|
|
508
|
+
findings.push(finding({
|
|
509
|
+
severity: 'error',
|
|
510
|
+
code: 'content-scan-incomplete',
|
|
511
|
+
repo: target.name,
|
|
512
|
+
message: `push evidence exceeds the ${CHECK_AGGREGATE_MAX_BYTES}-byte aggregate limit`,
|
|
513
|
+
}))
|
|
514
|
+
break
|
|
515
|
+
}
|
|
516
|
+
}
|
|
486
517
|
const errors = findings.filter((item) => item.severity === 'error')
|
|
487
518
|
return { ok: errors.length === 0, repo: target.name, updates, findings, errors, warnings: findings.filter((item) => item.severity !== 'error') }
|
|
488
519
|
}
|
|
489
520
|
|
|
490
|
-
export function auditContentRules({ project, policy } = {}) {
|
|
521
|
+
export function auditContentRules({ project, policy, source = 'working-tree' } = {}) {
|
|
491
522
|
const rules = resolveContentRules(policy)
|
|
492
523
|
const exceptions = asArray(policy?.contentRuleExceptions)
|
|
493
524
|
const findings = []
|
|
525
|
+
const diagnostics = []
|
|
494
526
|
for (const repo of managedRepos(project)) {
|
|
495
|
-
if (!repo.path || !fs.existsSync(path.join(repo.path, '.git')))
|
|
496
|
-
|
|
527
|
+
if (!repo.path || !fs.existsSync(path.join(repo.path, '.git'))) {
|
|
528
|
+
diagnostics.push(unscannableRepoFinding(repo, 'configured path is absent or is not a Git checkout'))
|
|
529
|
+
continue
|
|
530
|
+
}
|
|
531
|
+
const result = scanTree(repo.path, { rules, exceptions, repo: repo.name, source })
|
|
532
|
+
findings.push(...result.findings)
|
|
533
|
+
diagnostics.push(...result.diagnostics)
|
|
497
534
|
}
|
|
498
535
|
const accepted = []
|
|
499
536
|
for (const repo of managedRepos(project)) {
|
|
@@ -502,7 +539,16 @@ export function auditContentRules({ project, policy } = {}) {
|
|
|
502
539
|
accepted.push({ repo: repo.name, rule: exception.rule, paths: exception.paths, reason: exception.reason })
|
|
503
540
|
}
|
|
504
541
|
}
|
|
505
|
-
return { schema: BOUNDARY_POLICY_SCHEMA, findings, accepted }
|
|
542
|
+
return { schema: BOUNDARY_POLICY_SCHEMA, source, findings, diagnostics, accepted }
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
function unscannableRepoFinding(repo, reason) {
|
|
546
|
+
return finding({
|
|
547
|
+
severity: 'error',
|
|
548
|
+
code: 'repo-unscannable',
|
|
549
|
+
repo: repo?.name ?? null,
|
|
550
|
+
message: `${repo?.name ?? '(unnamed repo)'} cannot be scanned: ${reason}`,
|
|
551
|
+
})
|
|
506
552
|
}
|
|
507
553
|
|
|
508
554
|
function promotionRecords(project, policy) {
|
|
@@ -560,21 +606,19 @@ function promotionFindings({ policy, project, graph }) {
|
|
|
560
606
|
export function checkBoundaryPolicy({ project, policy, staged = false, stagedOnly = false, actor = null } = {}) {
|
|
561
607
|
const validationErrors = validateBoundaryPolicy(policy, project)
|
|
562
608
|
let graph = null
|
|
563
|
-
const findings = validationErrors.map((message) => finding({ severity:
|
|
564
|
-
if (
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
findings.push(...stagedContentFindings({ policy, project }))
|
|
577
|
-
}
|
|
609
|
+
const findings = validationErrors.map((message) => finding({ severity: 'error', code: 'boundary-policy-invalid', message }))
|
|
610
|
+
if (!stagedOnly) {
|
|
611
|
+
graph = buildGraph(project)
|
|
612
|
+
findings.push(...graph.errors.map((message) => finding({ severity: 'error', code: 'knowledge-graph-invalid', message })))
|
|
613
|
+
for (const node of graph.nodes ?? []) findings.push(...nodePlacementFindings({ node, policy }))
|
|
614
|
+
findings.push(...promotionFindings({ policy, project, graph }))
|
|
615
|
+
}
|
|
616
|
+
findings.push(...actorFindings({ policy, project, actor }))
|
|
617
|
+
if (staged) {
|
|
618
|
+
const stagedPaths = stagedPathsForProject(project)
|
|
619
|
+
findings.push(...forbiddenPathFindings({ policy, stagedPaths }))
|
|
620
|
+
findings.push(...semanticDiffFindings({ policy, project }))
|
|
621
|
+
findings.push(...stagedContentFindings({ policy, project }))
|
|
578
622
|
}
|
|
579
623
|
const errors = findings.filter((item) => item.severity === 'error')
|
|
580
624
|
const warnings = findings.filter((item) => item.severity !== 'error')
|
|
@@ -616,12 +660,12 @@ export function installBoundaryHooks({ project, force = false } = {}) {
|
|
|
616
660
|
const skipped = []
|
|
617
661
|
for (const repo of managedRepos(project)) {
|
|
618
662
|
if (!repo.path || !fs.existsSync(path.join(repo.path, '.git'))) continue
|
|
619
|
-
const hooksDir =
|
|
663
|
+
const hooksDir = gitHooksDir(repo.path)
|
|
620
664
|
fs.mkdirSync(hooksDir, { recursive: true })
|
|
621
665
|
for (const hookName of ['pre-commit', 'pre-push']) {
|
|
622
666
|
const hookPath = path.join(hooksDir, hookName)
|
|
623
667
|
const existing = fs.existsSync(hookPath) ? fs.readFileSync(hookPath, 'utf8') : ''
|
|
624
|
-
const script =
|
|
668
|
+
const script = boundaryHookScript(project.configPath, hookName, repo.path)
|
|
625
669
|
if (existing && !existing.includes('MNSTRY_ATELIER_BOUNDARY_GUARD') && !force) {
|
|
626
670
|
const sidecar = `${hookPath}.mnstry-atelier-boundary`
|
|
627
671
|
fs.writeFileSync(sidecar, script)
|
|
@@ -637,8 +681,32 @@ export function installBoundaryHooks({ project, force = false } = {}) {
|
|
|
637
681
|
return { installed, skipped }
|
|
638
682
|
}
|
|
639
683
|
|
|
640
|
-
function
|
|
641
|
-
const
|
|
684
|
+
function gitHooksDir(repoPath) {
|
|
685
|
+
const result = spawnSync('git', ['-C', repoPath, 'rev-parse', '--git-path', 'hooks'], { encoding: 'utf8' })
|
|
686
|
+
if (result.status !== 0 || !result.stdout.trim()) throw new Error(`cannot resolve Git hooks directory for ${repoPath}`)
|
|
687
|
+
const value = result.stdout.trim()
|
|
688
|
+
return path.isAbsolute(value) ? value : path.resolve(repoPath, value)
|
|
689
|
+
}
|
|
690
|
+
|
|
691
|
+
function shellSingleQuote(value) {
|
|
692
|
+
const text = String(value ?? '')
|
|
693
|
+
if (!text || /[\0\r\n]/.test(text)) throw new Error('project config path contains unsupported control characters')
|
|
694
|
+
return `'${text.replaceAll("'", `'"'"'`)}'`
|
|
695
|
+
}
|
|
696
|
+
|
|
697
|
+
export function boundaryHookScript(projectConfigPath, hookName, repoPath) {
|
|
698
|
+
const relativeConfigPath = projectConfigPath && repoPath ? path.relative(repoPath, projectConfigPath) : null
|
|
699
|
+
const configLivesInRepo = relativeConfigPath && relativeConfigPath !== '..' && !relativeConfigPath.startsWith(`..${path.sep}`) && !path.isAbsolute(relativeConfigPath)
|
|
700
|
+
const portableRelativeConfig = configLivesInRepo ? relativeConfigPath.replaceAll(path.sep, '/') : null
|
|
701
|
+
const configSetup = configLivesInRepo
|
|
702
|
+
? `ATELIER_HOOK_REPO_ROOT="$(git rev-parse --show-toplevel)"
|
|
703
|
+
ATELIER_HOOK_PROJECT_CONFIG="$ATELIER_HOOK_REPO_ROOT"/${shellSingleQuote(portableRelativeConfig)}`
|
|
704
|
+
: ''
|
|
705
|
+
const config = configLivesInRepo
|
|
706
|
+
? '--project-config="$ATELIER_HOOK_PROJECT_CONFIG"'
|
|
707
|
+
: projectConfigPath
|
|
708
|
+
? `--project-config=${shellSingleQuote(projectConfigPath)}`
|
|
709
|
+
: ''
|
|
642
710
|
// pre-push reads the ref updates git writes to stdin and judges only that range;
|
|
643
711
|
// pre-commit judges the staged diff. Neither scans the whole tree — that view is
|
|
644
712
|
// `atelier boundary audit`, which reports without blocking.
|
|
@@ -646,6 +714,7 @@ function hookScript(projectConfigPath, hookName) {
|
|
|
646
714
|
return `#!/usr/bin/env bash
|
|
647
715
|
# MNSTRY_ATELIER_BOUNDARY_GUARD ${hookName}
|
|
648
716
|
set -euo pipefail
|
|
717
|
+
${configSetup}
|
|
649
718
|
if command -v atelier >/dev/null 2>&1; then
|
|
650
719
|
exec atelier ${invocation}
|
|
651
720
|
elif [ -x "./node_modules/.bin/atelier" ]; then
|
|
@@ -663,7 +732,7 @@ fi
|
|
|
663
732
|
`
|
|
664
733
|
}
|
|
665
734
|
|
|
666
|
-
export function runBoundaryPushCheckCommand(argv = process.argv.slice(2), { stdin =
|
|
735
|
+
export function runBoundaryPushCheckCommand(argv = process.argv.slice(2), { stdin = readBoundaryPushInput() } = {}) {
|
|
667
736
|
const args = parseArgs(argv)
|
|
668
737
|
const project = commandProject({ argv })
|
|
669
738
|
const loaded = loadBoundaryPolicy(project)
|
|
@@ -671,12 +740,23 @@ export function runBoundaryPushCheckCommand(argv = process.argv.slice(2), { stdi
|
|
|
671
740
|
console.error(loaded.errors.join('\n'))
|
|
672
741
|
process.exit(1)
|
|
673
742
|
}
|
|
674
|
-
const
|
|
675
|
-
|
|
743
|
+
const input = stdin && typeof stdin === 'object' && Object.hasOwn(stdin, 'ok')
|
|
744
|
+
? stdin
|
|
745
|
+
: { ok: true, text: String(stdin ?? '') }
|
|
746
|
+
if (!input.ok) {
|
|
747
|
+
console.error('[boundary:push-check] could not read pre-push ref updates')
|
|
748
|
+
process.exit(1)
|
|
749
|
+
}
|
|
750
|
+
const parsed = parsePushRefInput(input.text)
|
|
751
|
+
if (!parsed.ok) {
|
|
752
|
+
console.error(`[boundary:push-check] invalid pre-push input: ${parsed.issues.join('; ')}`)
|
|
753
|
+
process.exit(1)
|
|
754
|
+
}
|
|
755
|
+
if (parsed.kind === 'empty') {
|
|
676
756
|
console.log('[boundary:push-check] no ref updates on stdin · nothing to judge')
|
|
677
757
|
process.exit(0)
|
|
678
758
|
}
|
|
679
|
-
const report = checkPushContent({ project, policy: loaded.policy, updates, cwd: process.cwd() })
|
|
759
|
+
const report = checkPushContent({ project, policy: loaded.policy, updates: parsed.updates, cwd: process.cwd() })
|
|
680
760
|
if (args.json) {
|
|
681
761
|
console.log(JSON.stringify(report, null, 2))
|
|
682
762
|
} else {
|
|
@@ -691,6 +771,10 @@ export function runBoundaryPushCheckCommand(argv = process.argv.slice(2), { stdi
|
|
|
691
771
|
process.exit(report.ok ? 0 : 1)
|
|
692
772
|
}
|
|
693
773
|
|
|
774
|
+
export function resolveBoundaryAuditSource(args = {}) {
|
|
775
|
+
return args.head ? 'head' : firstString(args.source) || 'working-tree'
|
|
776
|
+
}
|
|
777
|
+
|
|
694
778
|
export function runBoundaryAuditCommand(argv = process.argv.slice(2)) {
|
|
695
779
|
const args = parseArgs(argv)
|
|
696
780
|
const project = commandProject({ argv })
|
|
@@ -699,23 +783,29 @@ export function runBoundaryAuditCommand(argv = process.argv.slice(2)) {
|
|
|
699
783
|
console.error(loaded.errors.join('\n'))
|
|
700
784
|
process.exit(1)
|
|
701
785
|
}
|
|
702
|
-
const
|
|
786
|
+
const source = resolveBoundaryAuditSource(args)
|
|
787
|
+
if (!['working-tree', 'head'].includes(source)) {
|
|
788
|
+
console.error('boundary audit --source must be working-tree or head')
|
|
789
|
+
process.exit(1)
|
|
790
|
+
}
|
|
791
|
+
const report = auditContentRules({ project, policy: loaded.policy, source })
|
|
703
792
|
if (args.json) {
|
|
704
793
|
console.log(JSON.stringify(report, null, 2))
|
|
705
794
|
} else {
|
|
706
|
-
console.log(`[boundary:audit] ${report.findings.length} content-rule matches
|
|
795
|
+
console.log(`[boundary:audit] ${report.findings.length} content-rule matches · ${report.diagnostics.length} incomplete reads · ${report.accepted.length} declared exceptions · source ${report.source}`)
|
|
707
796
|
for (const item of report.findings.slice(0, 100)) console.log(`${item.severity} ${item.rule}: ${item.message}`)
|
|
797
|
+
for (const item of report.diagnostics.slice(0, 100)) console.log(`${item.severity} ${item.code}: ${item.message}`)
|
|
708
798
|
for (const item of report.accepted) console.log(`accepted ${item.rule} in ${item.repo} (${item.paths.join(', ')}): ${item.reason}`)
|
|
709
799
|
}
|
|
710
800
|
// Reporting only. A pre-existing accepted usage must never block unrelated work.
|
|
711
801
|
process.exit(0)
|
|
712
802
|
}
|
|
713
803
|
|
|
714
|
-
function
|
|
804
|
+
export function readBoundaryPushInput(reader = () => fs.readFileSync(0, 'utf8')) {
|
|
715
805
|
try {
|
|
716
|
-
return
|
|
806
|
+
return { ok: true, text: reader() }
|
|
717
807
|
} catch {
|
|
718
|
-
return ''
|
|
808
|
+
return { ok: false, text: '', error: 'stdin-read-failed' }
|
|
719
809
|
}
|
|
720
810
|
}
|
|
721
811
|
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import path from 'node:path'
|
|
4
|
+
import { pathToFileURL } from 'node:url'
|
|
5
|
+
|
|
6
|
+
const [, , requestedScript, ...args] = process.argv
|
|
7
|
+
|
|
8
|
+
function renderError(error) {
|
|
9
|
+
const debug = process.env.ATELIER_DEBUG === '1'
|
|
10
|
+
if (debug) {
|
|
11
|
+
console.error(error?.stack || String(error))
|
|
12
|
+
return Number.isInteger(error?.exitCode) ? error.exitCode : 1
|
|
13
|
+
}
|
|
14
|
+
if (typeof error?.code === 'string' && error.code) {
|
|
15
|
+
console.error(`[${error.code}] ${error.message}`)
|
|
16
|
+
if (error.hint) console.error(`Next: ${error.hint}`)
|
|
17
|
+
return Number.isInteger(error.exitCode) ? error.exitCode : 2
|
|
18
|
+
}
|
|
19
|
+
console.error('[internal-error] command failed without a safe diagnostic')
|
|
20
|
+
console.error('Next: rerun with ATELIER_DEBUG=1 to inspect the stack locally.')
|
|
21
|
+
return 1
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
if (!requestedScript) {
|
|
25
|
+
console.error('[command-missing] no command module was selected')
|
|
26
|
+
process.exit(2)
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const scriptPath = path.resolve(requestedScript)
|
|
30
|
+
process.argv = [process.execPath, scriptPath, ...args]
|
|
31
|
+
|
|
32
|
+
try {
|
|
33
|
+
await import(pathToFileURL(scriptPath).href)
|
|
34
|
+
} catch (error) {
|
|
35
|
+
process.exitCode = renderError(error)
|
|
36
|
+
}
|
package/src/cli/run.mjs
CHANGED
|
@@ -51,6 +51,8 @@ export const commandMap = new Map([
|
|
|
51
51
|
['extension-pack:list', ['src/commands/extension-pack.mjs', 'list']],
|
|
52
52
|
['distribution', ['src/commands/distribution.mjs']],
|
|
53
53
|
['distribution:check', ['src/commands/distribution.mjs', 'check']],
|
|
54
|
+
['disclosure', ['src/commands/disclosure.mjs']],
|
|
55
|
+
['disclosure:check', ['src/commands/disclosure.mjs', 'check']],
|
|
54
56
|
['attestation', ['src/commands/attestation.mjs']],
|
|
55
57
|
['feedback', ['src/commands/feedback.mjs']],
|
|
56
58
|
['feedback:check', ['src/commands/feedback.mjs', 'check']],
|
|
@@ -125,6 +127,7 @@ Core commands:
|
|
|
125
127
|
extension-pack validate Validate declared extension packs.
|
|
126
128
|
extension-pack list List declared extension packs.
|
|
127
129
|
distribution check Check a distribution for MNSTRY attribution.
|
|
130
|
+
disclosure check Scan tracked or staged content for disclosure risks.
|
|
128
131
|
|
|
129
132
|
Attestation commands:
|
|
130
133
|
attestation hash FILE Print the canonical payload hash of a payload.
|
|
@@ -132,8 +135,10 @@ Attestation commands:
|
|
|
132
135
|
attestation verify FILE Verify an attestation against a public key file.
|
|
133
136
|
attestation keygen --key-id ID Generate a signing key pair.
|
|
134
137
|
|
|
135
|
-
|
|
136
|
-
|
|
138
|
+
Commands whose usage names --project accept --project=PATH or --project PATH.
|
|
139
|
+
The project resolver also accepts --project-config=PATH and
|
|
140
|
+
MNSTRY_ATELIER_PROJECT_CONFIG=PATH; each command's own help is authoritative.
|
|
141
|
+
Machine-local repo paths belong in
|
|
137
142
|
.atelier-local/, atelier.local.json, or atelier.workspace.local.json.`
|
|
138
143
|
}
|
|
139
144
|
|
|
@@ -152,9 +157,9 @@ Ensures ignored local Atelier state exists, verifies ignore coverage, and record
|
|
|
152
157
|
doctor: `Usage: ${c} doctor [--project ./atelier.project.json] [--fix] [--dry-run]
|
|
153
158
|
|
|
154
159
|
Reports project config, local overlay, and repo boundary readiness. --fix only repairs ignored local state.`,
|
|
155
|
-
boundary: `Usage: ${c} boundary check|push-check|audit|install-hooks [--project ./atelier.project.json] [--staged]
|
|
160
|
+
boundary: `Usage: ${c} boundary check|push-check|audit|install-hooks [--project ./atelier.project.json] [--staged] [--source=working-tree|head]
|
|
156
161
|
|
|
157
|
-
check --staged judges the staged diff. push-check reads pre-push ref updates on stdin and judges only the pushed range. audit scans the
|
|
162
|
+
check --staged judges the staged diff. push-check reads pre-push ref updates on stdin and judges only the pushed range. audit scans the working tree by default and reports without blocking; --source=head selects the committed snapshot.`,
|
|
158
163
|
graph: `Usage: ${c} graph [--check] [--project ./atelier.project.json]
|
|
159
164
|
|
|
160
165
|
Builds or checks the project knowledge graph from tracked sources plus ignored local path bindings.`,
|
|
@@ -175,18 +180,21 @@ Loads every extension pack declared under ext["mnstry.atelier"].extensionPacks i
|
|
|
175
180
|
distribution: `Usage: ${c} distribution check [--target DIR] [--pack DIR]
|
|
176
181
|
|
|
177
182
|
Checks a distribution package for the required MNSTRY attribution markers. Blocking: the distribution README.md byte check, and a CLI probe that EXECUTES the target's declared bin with --version (spawned with the current Node, cwd set to the target — only run this against distributions you trust) and requires the attribution in its output; a target that looks like a distribution but declares no probe-able bin, or ships a malformed package.json, is also blocking. The extension-pack manifest attribution key is advisory and reported only. The normative wording lives in TRADEMARKS.md under "Required attribution"; see also docs/attestation.md and docs/distributions.md.`,
|
|
183
|
+
disclosure: `Usage: ${c} disclosure check [--root DIR] [--staged] [--denylist FILE | --structural-only] [--fail-on-binary] [--untrusted]
|
|
184
|
+
|
|
185
|
+
Scans Git-tracked files, or staged index blobs with --staged, without following symlinks. A private denylist is required by default and must be supplied through ATELIER_DENYLIST_JSON, --denylist, or ignored .atelier-local/disclosure-denylist.json. --structural-only is the explicit no-denylist lane. --untrusted suppresses finding details.`,
|
|
178
186
|
attestation: `Usage:
|
|
179
187
|
${c} attestation hash <payload.json>
|
|
180
188
|
${c} attestation sign <attestation.json> [--key FILE] [--out FILE]
|
|
181
189
|
${c} attestation verify <attestation.json> --public-key FILE [--payload FILE] [--json]
|
|
182
190
|
${c} attestation keygen --key-id ID [--algorithm ed25519|es256] [--out FILE]
|
|
183
191
|
|
|
184
|
-
Records and checks admission decisions. hash prints the canonical payload hash (RFC 8785 JCS, SHA-256). sign reads the local signing key file. verify reads a public key file and exits 1 when it judges the attestation invalid. keygen writes the signing key file mode 0600, refuses to overwrite, and prints only the public key document.`,
|
|
192
|
+
Records and checks admission decisions. hash prints the canonical payload hash (RFC 8785 JCS, SHA-256). sign reads the local signing key file. verify reads a public key file and exits 1 when it judges the attestation invalid. keygen writes the signing key file mode 0600 on POSIX, refuses to overwrite, and prints only the public key document.`,
|
|
185
193
|
feedback: `Usage:
|
|
186
194
|
${c} feedback create --message TEXT | --message-file PATH [--context FILE] [--include-gates]
|
|
187
195
|
${c} feedback check FILE
|
|
188
196
|
|
|
189
|
-
Assembles a local feedback report under ignored .atelier-local/feedback/ (mode 0600), scanned with the support-bundle banned key and value patterns before writing; any match refuses the write naming pattern label and location only. Files given to --message-file and --context must be valid UTF-8 text of at most 262144 bytes. The scan is a backstop, not clearance: read the whole report before sharing it. The kit has no send path — sharing the file is always the user's own explicit act.`,
|
|
197
|
+
Assembles a local feedback report under ignored .atelier-local/feedback/ (mode 0600 on POSIX), scanned with the support-bundle banned key and value patterns before writing; any match refuses the write naming pattern label and location only. Files given to --message-file and --context must be valid UTF-8 text of at most 262144 bytes. The scan is a backstop, not clearance: read the whole report before sharing it. The kit has no send path — sharing the file is always the user's own explicit act.`,
|
|
190
198
|
announcements: `Usage: ${c} announcements list [--dir DIR] [--public-key FILE] | verify <file> [--public-key FILE] [--json] | show <file> [--public-key FILE]
|
|
191
199
|
|
|
192
200
|
MNSTRY announcements are a pull-only channel: signed JSON documents under announcements/ in the repository. The trust anchor is always the committed MNSTRY key, or one you pass explicitly with --public-key; --dir changes only where documents are read from and never which key verifies them. Every run names the key and keyId it used. The kit never fetches anything — receiving announcements is the git pull you chose to run, and show refuses to print a body whose signature does not verify.`,
|
|
@@ -219,6 +227,7 @@ function normalizeArgs(argv) {
|
|
|
219
227
|
if (args[0] === 'extension-pack' && args[1] === 'validate') args.splice(0, 2, 'extension-pack:validate')
|
|
220
228
|
if (args[0] === 'extension-pack' && args[1] === 'list') args.splice(0, 2, 'extension-pack:list')
|
|
221
229
|
if (args[0] === 'distribution' && args[1] === 'check') args.splice(0, 2, 'distribution:check')
|
|
230
|
+
if (args[0] === 'disclosure' && args[1] === 'check') args.splice(0, 2, 'disclosure:check')
|
|
222
231
|
if (args[0] === 'support' && args[1] === 'bundle') args.splice(0, 2, 'support:bundle')
|
|
223
232
|
if (args[0] === 'support:bundle' && args[1] === '--dry-run') args.splice(1, 1)
|
|
224
233
|
return { help: false, args }
|
|
@@ -261,12 +270,13 @@ export async function runCli({
|
|
|
261
270
|
|
|
262
271
|
const [script, ...prefixArgs] = target
|
|
263
272
|
const scriptPath = path.join(packageRoot, script)
|
|
273
|
+
const executorPath = path.join(packageRoot, 'src', 'cli', 'execute-command.mjs')
|
|
264
274
|
if (!fs.existsSync(scriptPath)) {
|
|
265
275
|
stderr(`${brand.displayName} command is not available in this package install: ${command}`)
|
|
266
276
|
return 1
|
|
267
277
|
}
|
|
268
278
|
|
|
269
|
-
const result = spawnSync(process.execPath, [scriptPath, ...prefixArgs, ...rest], {
|
|
279
|
+
const result = spawnSync(process.execPath, [executorPath, scriptPath, ...prefixArgs, ...rest], {
|
|
270
280
|
cwd,
|
|
271
281
|
stdio: 'inherit',
|
|
272
282
|
env: {
|