@descix/cli 1.0.12 → 1.0.13
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/bin/descix.js +5 -0
- package/lib/core/SiteDenyLint.js +69 -0
- package/package.json +1 -1
package/bin/descix.js
CHANGED
|
@@ -2537,6 +2537,11 @@ siteCommand
|
|
|
2537
2537
|
return;
|
|
2538
2538
|
}
|
|
2539
2539
|
|
|
2540
|
+
// 1b. Refuse to publish backend source, an agent's persona, credentials or workspace
|
|
2541
|
+
// state — BEFORE any token is requested, so a dry run refuses too (SiteDenyLint).
|
|
2542
|
+
const { assertSitePublishable } = await import('../lib/core/SiteDenyLint.js');
|
|
2543
|
+
assertSitePublishable(fileList.map((f) => f.path));
|
|
2544
|
+
|
|
2540
2545
|
// 2. Request deploy token with file list
|
|
2541
2546
|
const tokenResponse = await apiClient.invoke('get_site_deploy_token', {
|
|
2542
2547
|
community_id: communityId,
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What a site upload must never publish. Hard deny — the upload fails loud before any file
|
|
3
|
+
* leaves the machine, dry-run included.
|
|
4
|
+
*
|
|
5
|
+
* WHY (measured, 2026-09-18): egpt-godsworld's DEV site had been publicly serving, as static
|
|
6
|
+
* files, the app's persona (`assets/system_instructions.md`) and its whole backend source tree
|
|
7
|
+
* (`microservice/app.js`, `Dockerfile`, `services/*`, config examples). An earlier upload used a
|
|
8
|
+
* broad manifest, and `site upload` publishes whatever its manifest names. The KB sync already
|
|
9
|
+
* refuses private content (CorpusDenyLint); the site upload had no equivalent.
|
|
10
|
+
*
|
|
11
|
+
* A DIFFERENT PATTERN SET from the corpus, on purpose: the corpus lint is about editorial
|
|
12
|
+
* publishability (drafts, patents, test dumps), and a site legitimately ships some of those. This
|
|
13
|
+
* set is about what is private at RUNTIME — backend source, an agent's persona, credentials,
|
|
14
|
+
* workspace and tooling state. The MATCHER is shared (CorpusDenyLint.matchPathPattern), so there
|
|
15
|
+
* is one definition of how a pattern meets a path.
|
|
16
|
+
*
|
|
17
|
+
* No exemption flag: every class here is repaired by not naming the file in the manifest, which
|
|
18
|
+
* is always possible. An escape hatch would be the one path the next leak takes.
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
import { matchPathPattern } from './CorpusDenyLint.js';
|
|
22
|
+
|
|
23
|
+
export const SITE_DENY_PATH_PATTERNS = [
|
|
24
|
+
{ id: 1, class: 'app-source', patterns: ['**/microservice/**', 'microservice'],
|
|
25
|
+
reason: 'backend source — an app\'s microservice is deployed as a service, never served as files' },
|
|
26
|
+
{ id: 2, class: 'agent-persona', patterns: ['system_instructions.md'],
|
|
27
|
+
reason: 'the agent\'s persona / system instructions — its prompt is not a public asset' },
|
|
28
|
+
{ id: 3, class: 'credential', caseFold: true,
|
|
29
|
+
patterns: ['wallet*.json', '.env', '.env.*', 'dev-overrides*.json', 'secret*.json', '*.pem', '*.key',
|
|
30
|
+
'service-account*.json', 'credentials*.json', 'id_rsa*', '*.p12'],
|
|
31
|
+
reason: 'credential or credential-shaped config (a placeholder copy teaches the real layout)' },
|
|
32
|
+
{ id: 4, class: 'workspace-state', patterns: ['**/.descix/**', '.descix'],
|
|
33
|
+
reason: 'DeSciX workspace / session state' },
|
|
34
|
+
{ id: 5, class: 'tooling-state', patterns: ['**/.claude/**', '**/.git/**', '.claude', '.git'],
|
|
35
|
+
reason: 'agent or VCS internals' },
|
|
36
|
+
];
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Every published path that matches a deny class.
|
|
40
|
+
* @param {string[]} publishedPaths - paths relative to the site root, as they would be served
|
|
41
|
+
* @returns {Array<{path:string, class:string, glob:string, reason:string}>}
|
|
42
|
+
*/
|
|
43
|
+
export function lintSiteFiles(publishedPaths) {
|
|
44
|
+
const violations = [];
|
|
45
|
+
for (const p of publishedPaths) {
|
|
46
|
+
for (const pattern of SITE_DENY_PATH_PATTERNS) {
|
|
47
|
+
const glob = matchPathPattern(p, pattern);
|
|
48
|
+
if (glob) {
|
|
49
|
+
violations.push({ path: p, class: pattern.class, glob, reason: pattern.reason });
|
|
50
|
+
break;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
return violations;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/** Throw, naming every offending file and the repair, when any path is denied. */
|
|
58
|
+
export function assertSitePublishable(publishedPaths) {
|
|
59
|
+
const violations = lintSiteFiles(publishedPaths);
|
|
60
|
+
if (violations.length === 0) return;
|
|
61
|
+
const lines = violations.map((v) => ` - ${v.path} [${v.class}] ${v.reason}`);
|
|
62
|
+
const err = new Error(
|
|
63
|
+
`site upload REFUSED: ${violations.length} file(s) must never be publicly served:\n${lines.join('\n')}\n` +
|
|
64
|
+
'Nothing was uploaded. Remove these from the site manifest\'s sources (.descix/manifests/site.json) ' +
|
|
65
|
+
'or from the build output, then upload again.');
|
|
66
|
+
err.code = 'SITE_DENY_PATH';
|
|
67
|
+
err.violations = violations;
|
|
68
|
+
throw err;
|
|
69
|
+
}
|