@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.
Files changed (53) hide show
  1. package/CHANGELOG.md +62 -0
  2. package/README.md +38 -12
  3. package/contracts/public-api-baseline.json +57 -0
  4. package/docs/assurance-controls.md +39 -0
  5. package/docs/atelier-runtime.md +15 -0
  6. package/docs/blocks/claims.md +15 -9
  7. package/docs/design.md +12 -6
  8. package/docs/install.md +26 -4
  9. package/docs/knowledge-graph.md +8 -4
  10. package/docs/local-services.md +101 -0
  11. package/docs/release-engineering.md +75 -10
  12. package/docs/repo-boundary-guard.md +12 -2
  13. package/docs/upgrade.md +25 -2
  14. package/fixtures/projects/sample-workspace/content/source.html.kg.json +4 -1
  15. package/fixtures/projects/source-formats-workspace/content/data.json.kg.json +4 -1
  16. package/fixtures/projects/source-formats-workspace/content/logo.png.kg.json +4 -1
  17. package/fixtures/projects/source-formats-workspace/content/metrics.csv.kg.json +4 -1
  18. package/fixtures/projects/source-formats-workspace/content/pipeline.yaml.kg.json +4 -1
  19. package/package.json +12 -5
  20. package/skills/claude/atelier-local-service/SKILL.md +47 -0
  21. package/skills/claude/atelier-public-boundary/SKILL.md +31 -0
  22. package/skills/codex/atelier-local-service/SKILL.md +47 -0
  23. package/skills/codex/atelier-public-boundary/SKILL.md +31 -0
  24. package/src/boundary/content-rules.mjs +278 -20
  25. package/src/boundary/policy.mjs +150 -60
  26. package/src/cli/execute-command.mjs +36 -0
  27. package/src/cli/run.mjs +17 -7
  28. package/src/collaboration/event-ledger.mjs +365 -0
  29. package/src/collaboration/index.mjs +17 -0
  30. package/src/collaboration/proposals.mjs +265 -65
  31. package/src/commands/attestation.mjs +20 -6
  32. package/src/commands/disclosure.mjs +133 -0
  33. package/src/commands/distribution.mjs +2 -1
  34. package/src/commands/extension-pack.mjs +2 -1
  35. package/src/commands/init.mjs +2 -1
  36. package/src/commands/server.mjs +1 -4
  37. package/src/disclosure/content-scan.mjs +193 -0
  38. package/src/egress/check.mjs +7 -38
  39. package/src/egress/forbidden-egress.mjs +32 -18
  40. package/src/graph/graph.mjs +112 -314
  41. package/src/graph/knowledge-graph.mjs +94 -18
  42. package/src/harness/context-client.mjs +9 -1
  43. package/src/index.mjs +12 -0
  44. package/src/project/config.mjs +66 -7
  45. package/src/project/file-class.mjs +14 -0
  46. package/src/project/package-root.mjs +10 -0
  47. package/src/project/path-match.mjs +38 -15
  48. package/src/project/private-state.mjs +110 -0
  49. package/src/server/local-sidecar.mjs +81 -59
  50. package/src/server/security.mjs +89 -4
  51. package/src/server/server.mjs +3 -2
  52. package/src/support/feedback-report.mjs +4 -3
  53. package/src/upgrade/upgrade.mjs +2 -1
@@ -0,0 +1,193 @@
1
+ import { execFileSync } from 'node:child_process'
2
+ import fs from 'node:fs'
3
+ import path from 'node:path'
4
+
5
+ export const STRUCTURAL_DISCLOSURE_PATTERNS = Object.freeze([
6
+ { pattern: new RegExp('\\/Users\\/'), label: 'absolute user path' },
7
+ { pattern: new RegExp('\\/var\\/folders\\/'), label: 'machine-local temp path' },
8
+ { pattern: new RegExp('\\.' + 'codex'), label: 'agent-local state path' },
9
+ {
10
+ pattern: new RegExp([
11
+ 'BEGIN ',
12
+ '(?:[A-Z0-9]+ ){0,4}',
13
+ 'PRIVATE KEY(?: BLOCK)?',
14
+ '|BEGIN (?:RSA|OPENSSH) KEY',
15
+ ].join('')),
16
+ label: 'private key material',
17
+ },
18
+ { pattern: /"d"\s*:\s*"[A-Za-z0-9_-]{20,}"/, label: 'JWK private key material' },
19
+ {
20
+ pattern: /\b(api[_-]?key|secret|password|(?<!id-)token)\b\s*[:=]/i,
21
+ label: 'secret-like assignment',
22
+ },
23
+ ])
24
+
25
+ export function compileDisclosurePatterns(patternDocs = []) {
26
+ if (!Array.isArray(patternDocs)) throw new Error('denylist document must contain a patterns array')
27
+ return patternDocs.map(({ pattern, flags = '', label }) => {
28
+ if (typeof label !== 'string' || label.length === 0) throw new Error('denylist pattern label must be non-empty')
29
+ if (typeof pattern !== 'string' || pattern.length === 0) throw new Error(`denylist pattern failed to compile: ${label}`)
30
+ try {
31
+ return { pattern: new RegExp(pattern, String(flags).replace(/[gy]/g, '')), label }
32
+ } catch {
33
+ throw new Error(`denylist pattern failed to compile: ${label}`)
34
+ }
35
+ })
36
+ }
37
+
38
+ export function scanDisclosureContent({
39
+ root = process.cwd(),
40
+ staged = false,
41
+ denylistPatterns = [],
42
+ failOnBinary = false,
43
+ } = {}) {
44
+ const resolvedRoot = path.resolve(root)
45
+ assertGitCheckout(resolvedRoot)
46
+ const patterns = [...STRUCTURAL_DISCLOSURE_PATTERNS, ...denylistPatterns]
47
+ const findings = []
48
+ const skippedBinary = []
49
+ let scannedFiles = 0
50
+
51
+ for (const entry of stagedEntries(resolvedRoot, staged)) {
52
+ const buffer = entry.buffer ?? readTrackedFile(resolvedRoot, entry.path)
53
+ if (buffer === null) continue
54
+ const text = decodeText(buffer)
55
+ if (text === null) {
56
+ skippedBinary.push(entry.path)
57
+ if (failOnBinary) findings.push({ label: 'binary content cannot be disclosure-scanned', path: entry.path, line: null })
58
+ continue
59
+ }
60
+
61
+ scannedFiles += 1
62
+ const lines = text.split('\n')
63
+ for (let index = 0; index < lines.length; index += 1) {
64
+ for (const { pattern, label } of patterns) {
65
+ if (pattern.test(lines[index])) findings.push({ label, path: entry.path, line: index + 1 })
66
+ }
67
+ }
68
+ }
69
+
70
+ return {
71
+ ok: findings.length === 0,
72
+ staged,
73
+ scannedFiles,
74
+ skippedBinary,
75
+ findings,
76
+ }
77
+ }
78
+
79
+ function decodeText(buffer) {
80
+ if (buffer.includes(0)) return null
81
+ try {
82
+ return new TextDecoder('utf-8', { fatal: true }).decode(buffer)
83
+ } catch {
84
+ return null
85
+ }
86
+ }
87
+
88
+ export function isPathTracked(root, candidatePath) {
89
+ const resolvedRoot = path.resolve(root)
90
+ const resolvedCandidate = path.resolve(candidatePath)
91
+ const relative = path.relative(resolvedRoot, resolvedCandidate)
92
+ if (relative.startsWith('..') || path.isAbsolute(relative)) return false
93
+ const gitRelative = relative.split(path.sep).join('/')
94
+ const result = execFileSync('git', ['-C', resolvedRoot, 'ls-files', '-z', '--', gitRelative], {
95
+ encoding: 'utf8',
96
+ stdio: ['ignore', 'pipe', 'pipe'],
97
+ })
98
+ if (result.split('\0').filter(Boolean).includes(gitRelative)) return true
99
+
100
+ // Git pathspec matching is case-sensitive even when the checkout filesystem
101
+ // is not. Compare filesystem identities so alternate-cased callers cannot
102
+ // make a tracked file appear local and untracked.
103
+ return execFileSync('git', [
104
+ '-C',
105
+ resolvedRoot,
106
+ 'ls-files',
107
+ '-z',
108
+ '--',
109
+ `:(icase,literal)${gitRelative}`,
110
+ ], {
111
+ encoding: 'utf8',
112
+ stdio: ['ignore', 'pipe', 'pipe'],
113
+ })
114
+ .split('\0')
115
+ .filter(Boolean)
116
+ .some((trackedPath) => sameExistingFile(resolvedCandidate, path.join(resolvedRoot, trackedPath)))
117
+ }
118
+
119
+ function sameExistingFile(left, right) {
120
+ try {
121
+ if (fs.realpathSync.native(left) === fs.realpathSync.native(right)) return true
122
+ const leftStat = fs.statSync(left)
123
+ const rightStat = fs.statSync(right)
124
+ return leftStat.ino !== 0 && leftStat.dev === rightStat.dev && leftStat.ino === rightStat.ino
125
+ } catch {
126
+ return false
127
+ }
128
+ }
129
+
130
+ export function isPathIgnored(root, candidatePath) {
131
+ const resolvedRoot = path.resolve(root)
132
+ const relative = path.relative(resolvedRoot, path.resolve(candidatePath))
133
+ if (relative.startsWith('..') || path.isAbsolute(relative)) return false
134
+ const gitRelative = relative.split(path.sep).join('/')
135
+ try {
136
+ execFileSync('git', ['-C', resolvedRoot, 'check-ignore', '--quiet', '--', gitRelative], {
137
+ stdio: ['ignore', 'ignore', 'ignore'],
138
+ })
139
+ return true
140
+ } catch {
141
+ return false
142
+ }
143
+ }
144
+
145
+ function assertGitCheckout(root) {
146
+ try {
147
+ execFileSync('git', ['-C', root, 'rev-parse', '--git-dir'], {
148
+ stdio: ['ignore', 'ignore', 'ignore'],
149
+ })
150
+ } catch {
151
+ throw new Error(`disclosure root is not a Git checkout`)
152
+ }
153
+ }
154
+
155
+ function stagedEntries(root, staged) {
156
+ if (!staged) {
157
+ return git(root, ['ls-files', '-z'])
158
+ .split('\0')
159
+ .filter(Boolean)
160
+ .map((filePath) => ({ path: filePath }))
161
+ }
162
+
163
+ return git(root, ['diff', '--cached', '--name-only', '--diff-filter=ACMR', '-z'])
164
+ .split('\0')
165
+ .filter(Boolean)
166
+ .map((filePath) => ({
167
+ path: filePath,
168
+ buffer: execFileSync('git', ['-C', root, 'show', `:${filePath}`], {
169
+ stdio: ['ignore', 'pipe', 'pipe'],
170
+ maxBuffer: 64 * 1024 * 1024,
171
+ }),
172
+ }))
173
+ }
174
+
175
+ function readTrackedFile(root, relativePath) {
176
+ const absolutePath = path.join(root, relativePath)
177
+ let stat
178
+ try {
179
+ stat = fs.lstatSync(absolutePath)
180
+ } catch {
181
+ return null
182
+ }
183
+ if (!stat.isFile()) return null
184
+ return fs.readFileSync(absolutePath)
185
+ }
186
+
187
+ function git(root, args) {
188
+ return execFileSync('git', ['-C', root, ...args], {
189
+ encoding: 'utf8',
190
+ stdio: ['ignore', 'pipe', 'pipe'],
191
+ maxBuffer: 64 * 1024 * 1024,
192
+ })
193
+ }
@@ -1,44 +1,13 @@
1
- import fs from 'node:fs'
2
- import path from 'node:path'
1
+ import { checkForbiddenEgress } from './forbidden-egress.mjs'
3
2
 
4
- const ROOT = path.resolve(new URL('../..', import.meta.url).pathname)
5
- const SCAN_DIRS = ['bin', 'src']
6
- const FORBIDDEN = [
7
- { pattern: /fetch\(\s*['"]https?:\/\/(?!127\.0\.0\.1|localhost|\[::1\])/, label: 'non-localhost fetch' },
8
- { pattern: /https?\.request\s*\(/, label: 'http request primitive' },
9
- { pattern: /new\s+WebSocket\s*\(/, label: 'websocket egress primitive' },
10
- { pattern: /net\.connect\s*\(/, label: 'net connect primitive' },
11
- { pattern: /dns\./, label: 'dns primitive' },
12
- { pattern: /\bcurl\s+https?:\/\//, label: 'shell network call' },
13
- ]
14
-
15
- function walk(dir, files = []) {
16
- if (!fs.existsSync(dir)) return files
17
- for (const ent of fs.readdirSync(dir, { withFileTypes: true })) {
18
- const abs = path.join(dir, ent.name)
19
- if (ent.isDirectory()) walk(abs, files)
20
- else if (/\.(mjs|js|sh)$/.test(ent.name)) files.push(abs)
21
- }
22
- return files
23
- }
24
-
25
- export function checkForbiddenEgress(root = ROOT) {
26
- const failures = []
27
- for (const rel of SCAN_DIRS) {
28
- for (const file of walk(path.join(root, rel))) {
29
- const text = fs.readFileSync(file, 'utf8')
30
- for (const rule of FORBIDDEN) {
31
- if (rule.pattern.test(text)) failures.push(`${path.relative(root, file)}:${rule.label}`)
32
- }
33
- }
34
- }
35
- return failures
36
- }
3
+ export { checkForbiddenEgress } from './forbidden-egress.mjs'
37
4
 
38
5
  export function runEgressCommand() {
39
- const failures = checkForbiddenEgress()
40
- if (failures.length) {
41
- console.error(failures.join('\n'))
6
+ const findings = checkForbiddenEgress()
7
+ if (findings.length) {
8
+ for (const finding of findings) {
9
+ console.error(`[atelier-egress] ${finding.file}:${finding.line} ${finding.type}: ${finding.detail}`)
10
+ }
42
11
  process.exit(1)
43
12
  }
44
13
  console.log('[egress:check] no forbidden non-localhost egress found in package runtime paths')
@@ -22,20 +22,21 @@ export const DEFAULT_EGRESS_SCAN_PATHS = [
22
22
  const SCRIPT_EXTS = new Set(['.js', '.mjs', '.cjs', '.ts', '.mts', '.sh'])
23
23
  const MARKUP_EXTS = new Set(['.html', '.htm', '.svg'])
24
24
  const SKIP_DIRS = new Set(['node_modules', '.git'])
25
- const INTERNAL_SCANNER_FILES = new Set(['forbidden-egress.mjs'])
25
+ const INTERNAL_SCANNER_PATH = 'src/egress/forbidden-egress.mjs'
26
26
 
27
27
  function isTestFile(file) {
28
28
  return /(?:^|[/.])test(?:s)?[/.]/.test(file) || /\.(?:test|spec)\.[cm]?[jt]s$/.test(file)
29
29
  }
30
30
 
31
- function isMarkedTestFixture(file, text) {
32
- return isTestFile(file) && text.includes(TEST_FIXTURE_ALLOW_MARKER)
31
+ function isMarkedTestFixture(file, text, allowTestFixtures) {
32
+ return allowTestFixtures && isTestFile(file) && text.includes(TEST_FIXTURE_ALLOW_MARKER)
33
33
  }
34
34
 
35
- function shouldScanFile(file, { includeTests = false } = {}) {
35
+ function shouldScanFile(file, { includeTests = true, root = packageRoot } = {}) {
36
36
  const ext = path.extname(file)
37
37
  if (!SCRIPT_EXTS.has(ext) && !MARKUP_EXTS.has(ext)) return false
38
- if (INTERNAL_SCANNER_FILES.has(path.basename(file))) return false
38
+ const relative = path.relative(path.resolve(root), path.resolve(file)).split(path.sep).join('/')
39
+ if (relative === INTERNAL_SCANNER_PATH) return false
39
40
  if (!includeTests && isTestFile(file)) return false
40
41
  return true
41
42
  }
@@ -61,17 +62,28 @@ function uniqueFiles(files) {
61
62
  export function discoverForbiddenEgressScanFiles({
62
63
  root = packageRoot,
63
64
  scanPaths = DEFAULT_EGRESS_SCAN_PATHS,
64
- includeTests = false,
65
+ includeTests = true,
66
+ files = null,
65
67
  } = {}) {
66
- const files = []
68
+ if (Array.isArray(files)) {
69
+ const resolvedRoot = path.resolve(root)
70
+ return uniqueFiles(
71
+ files
72
+ .map((file) => path.resolve(resolvedRoot, file))
73
+ .filter((file) => file === resolvedRoot || file.startsWith(`${resolvedRoot}${path.sep}`))
74
+ .filter((file) => fs.existsSync(file) && fs.statSync(file).isFile())
75
+ .filter((file) => shouldScanFile(file, { includeTests, root: resolvedRoot })),
76
+ )
77
+ }
78
+ const discovered = []
67
79
  for (const rel of scanPaths) {
68
80
  const abs = path.resolve(root, rel)
69
81
  if (!fs.existsSync(abs)) continue
70
82
  const stat = fs.statSync(abs)
71
- if (stat.isDirectory()) files.push(...walk(abs, { includeTests }))
72
- else if (shouldScanFile(abs, { includeTests })) files.push(abs)
83
+ if (stat.isDirectory()) discovered.push(...walk(abs, { includeTests, root }))
84
+ else if (shouldScanFile(abs, { includeTests, root })) discovered.push(abs)
73
85
  }
74
- return uniqueFiles(files)
86
+ return uniqueFiles(discovered)
75
87
  }
76
88
 
77
89
  function hostnameFromNetworkUrl(value) {
@@ -135,8 +147,8 @@ function callBlock(lines, index) {
135
147
  return lines.slice(index, Math.min(lines.length, index + 5)).join('\n')
136
148
  }
137
149
 
138
- function isFixtureAllowed(lines, index, file) {
139
- return isTestFile(file) && callBlock(lines, Math.max(0, index - 2)).includes(TEST_FIXTURE_ALLOW_MARKER)
150
+ function isFixtureAllowed(lines, index, file, allowTestFixtures) {
151
+ return allowTestFixtures && isTestFile(file) && callBlock(lines, Math.max(0, index - 2)).includes(TEST_FIXTURE_ALLOW_MARKER)
140
152
  }
141
153
 
142
154
  function isLocalComputedAllowed(lines, index) {
@@ -256,14 +268,14 @@ function markupFindingsForLine(line, { findings, file, lineNumber }) {
256
268
  }
257
269
  }
258
270
 
259
- export function forbiddenEgressFindingsForText(text, { file = 'input' } = {}) {
260
- if (isMarkedTestFixture(file, text)) return []
271
+ export function forbiddenEgressFindingsForText(text, { file = 'input', allowTestFixtures = true } = {}) {
272
+ if (isMarkedTestFixture(file, text, allowTestFixtures)) return []
261
273
  const findings = []
262
274
  const lines = String(text || '').split(/\r?\n/)
263
275
  const isMarkup = MARKUP_EXTS.has(path.extname(file))
264
276
 
265
277
  for (let index = 0; index < lines.length; index += 1) {
266
- if (isFixtureAllowed(lines, index, file)) continue
278
+ if (isFixtureAllowed(lines, index, file, allowTestFixtures)) continue
267
279
  const lineCode = trimmedCodeLine(lines[index])
268
280
  const hasJsPrimitive = /\b(?:fetch|https?\.(?:request|get)|http2\.connect|navigator\.sendBeacon|new\s+EventSource|new\s+WebSocket|net\.connect|net\.createConnection|dns\.(?:resolve|lookup|promises\.resolve|promises\.lookup)|import)\s*\(/.test(lineCode)
269
281
  || /\bnew\s+(?:XMLHttpRequest|Image)\b/.test(lineCode)
@@ -295,12 +307,14 @@ export function forbiddenEgressFindingsForText(text, { file = 'input' } = {}) {
295
307
  export function checkForbiddenEgress({
296
308
  root = packageRoot,
297
309
  scanPaths = DEFAULT_EGRESS_SCAN_PATHS,
298
- includeTests = false,
310
+ includeTests = true,
311
+ files = null,
312
+ allowTestFixtures = true,
299
313
  } = {}) {
300
314
  const findings = []
301
- for (const file of discoverForbiddenEgressScanFiles({ root, scanPaths, includeTests })) {
315
+ for (const file of discoverForbiddenEgressScanFiles({ root, scanPaths, includeTests, files })) {
302
316
  const rel = path.relative(root, file)
303
- findings.push(...forbiddenEgressFindingsForText(fs.readFileSync(file, 'utf8'), { file: rel }))
317
+ findings.push(...forbiddenEgressFindingsForText(fs.readFileSync(file, 'utf8'), { file: rel, allowTestFixtures }))
304
318
  }
305
319
  return findings
306
320
  }