chati-dev 4.5.15 → 4.5.27
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/README.md +8 -3
- package/bin/chati.js +124 -66
- package/framework/agents/build/dev.md +5 -5
- package/framework/agents/deploy/devops.md +7 -7
- package/framework/agents/discover/brief.md +4 -4
- package/framework/agents/discover/brownfield-wu.md +3 -3
- package/framework/agents/discover/greenfield-wu.md +3 -3
- package/framework/agents/plan/architect.md +2 -2
- package/framework/agents/plan/detail.md +4 -4
- package/framework/agents/plan/phases.md +2 -2
- package/framework/agents/plan/tasks.md +2 -2
- package/framework/agents/plan/ux.md +2 -2
- package/framework/agents/quality/qa-implementation.md +11 -10
- package/framework/agents/quality/qa-planning.md +3 -3
- package/framework/agents/quality/qa-visual.md +1 -1
- package/framework/config.yaml +3 -3
- package/framework/constitution.md +18 -18
- package/framework/context/protocols.md +2 -2
- package/framework/context/quality.md +1 -1
- package/framework/context/root.md +4 -4
- package/framework/data/entity-registry.yaml +1 -1
- package/framework/domains/agents/orchestrator.yaml +2 -2
- package/framework/domains/constitution.yaml +1 -1
- package/framework/hooks/advance-trigger.js +4 -6
- package/framework/hooks/git-push-authority.js +45 -37
- package/framework/hooks/mode-governance.js +149 -40
- package/framework/hooks/model-governance.js +13 -20
- package/framework/hooks/prism-engine.js +74 -92
- package/framework/hooks/reasoning-escalator.js +23 -40
- package/framework/hooks/session-digest.js +12 -13
- package/framework/hooks/session-reader.js +224 -0
- package/framework/hooks/session-writer.js +195 -0
- package/framework/hooks/team-quality-gate.js +34 -24
- package/framework/i18n/en.yaml +2 -2
- package/framework/i18n/es.yaml +2 -2
- package/framework/i18n/fr.yaml +2 -2
- package/framework/i18n/pt.yaml +2 -2
- package/framework/intelligence/context-engine.md +4 -4
- package/framework/intelligence/memory-layer.md +1 -1
- package/framework/manifest.json +129 -119
- package/framework/manifest.sig +1 -1
- package/framework/orchestrator/chati-router.js +278 -24
- package/framework/orchestrator/chati.md +250 -68
- package/framework/schemas/session.schema.json +21 -1
- package/framework/tasks/orchestrator-deviation.md +1 -1
- package/framework/tasks/orchestrator-escalate.md +1 -1
- package/framework/tasks/orchestrator-handoff.md +6 -6
- package/framework/tasks/orchestrator-health.md +5 -9
- package/framework/tasks/orchestrator-mode-switch.md +3 -7
- package/framework/tasks/orchestrator-resume.md +10 -14
- package/framework/tasks/orchestrator-route.md +3 -3
- package/framework/tasks/orchestrator-spawn-terminal.md +1 -1
- package/framework/tasks/orchestrator-status.md +9 -9
- package/framework/tasks/orchestrator-suggest-mode.md +1 -1
- package/framework/tasks/qa-impl-consolidate.md +2 -2
- package/framework/tasks/qa-impl-performance-test.md +4 -4
- package/framework/tasks/qa-impl-regression-check.md +4 -4
- package/framework/tasks/qa-impl-sast-scan.md +1 -1
- package/framework/tasks/qa-impl-test-execute.md +1 -1
- package/framework/tasks/qa-impl-verdict.md +2 -2
- package/framework/tasks/qa-planning-consolidate.md +3 -3
- package/framework/tasks/qa-planning-coverage-plan.md +2 -2
- package/framework/tasks/qa-planning-gate-define.md +4 -4
- package/framework/tasks/qa-planning-risk-matrix.md +4 -4
- package/framework/tasks/qa-planning-test-strategy.md +2 -2
- package/node_modules/@chati/browser-capability/src/index.js +12 -2
- package/node_modules/@chati/planning/src/index.js +24 -4
- package/node_modules/@chati/provider-registry/src/index.js +11 -0
- package/node_modules/@chati/rail/src/index.js +1967 -83
- package/node_modules/@chati/release-lane/README.md +12 -8
- package/node_modules/@chati/release-lane/package.json +1 -1
- package/node_modules/@chati/release-lane/src/index.js +1426 -58
- package/node_modules/@chati/review-council/src/index.js +63 -0
- package/node_modules/@chati/tracking-clickup/README.md +13 -0
- package/node_modules/@chati/tracking-clickup/src/index.js +690 -30
- package/package-artifact-manifest.json +1 -0
- package/package-artifact-manifest.sig +1 -0
- package/package.json +16 -7
- package/scripts/verify-real-harness-e2e.js +1581 -0
- package/src/config/framework-adapter.js +4 -4
- package/src/installer/core.js +157 -56
- package/src/installer/manifest.js +140 -9
- package/src/installer/package-artifact.js +249 -0
- package/src/installer/templates.js +91 -23
- package/src/installer-v2/catalog-client.js +531 -23
- package/src/installer-v2/index.js +91 -34
- package/src/installer-v2/installation-authority.js +327 -0
- package/src/installer-v2/provider-executable.js +235 -0
- package/src/installer-v2/wizard-installation.js +1 -1
- package/src/intelligence/timeline.js +3 -1
- package/src/orchestrator/browser-runtime.js +44 -13
- package/src/orchestrator/cli.js +1616 -151
- package/src/orchestrator/clickup-projection.js +43 -1
- package/src/orchestrator/clickup-runtime.js +355 -39
- package/src/orchestrator/doctor.js +87 -4
- package/src/orchestrator/index.js +16 -0
- package/src/orchestrator/planning-runtime.js +20 -2
- package/src/orchestrator/rail-adjudication-evidence.js +234 -0
- package/src/orchestrator/rail-evidence-authority.js +147 -0
- package/src/orchestrator/rail-execution-evidence.js +45 -0
- package/src/orchestrator/rail-runtime.js +836 -56
- package/src/orchestrator/release-runtime.js +65 -6
- package/src/orchestrator/review-runtime.js +186 -41
- package/src/orchestrator/runtime-installation-v2.js +236 -20
- package/src/orchestrator/session-manager.js +1249 -52
- package/src/terminal/adapters/claude-adapter.js +3 -1
- package/src/terminal/adapters/codex-adapter.js +2 -0
- package/src/terminal/adapters/grok-adapter.js +7 -4
- package/src/terminal/handoff-parser.js +19 -1
- package/src/terminal/prompt-builder.js +10 -1
- package/src/terminal/provider-preflight.js +36 -3
- package/src/terminal/rail-execution-worktree.js +213 -0
- package/src/terminal/rail-prompts.js +169 -0
- package/src/terminal/rail-readonly-workspace.js +324 -0
- package/src/terminal/run-agent.js +434 -16
- package/src/terminal/run-parallel.js +5 -0
- package/src/terminal/run-rail-adjudication.js +323 -0
- package/src/terminal/run-rail-review.js +291 -0
- package/src/terminal/run-rail-rework.js +325 -0
- package/src/terminal/run-rail-task.js +380 -0
- package/src/terminal/run-team.js +5 -0
- package/src/terminal/spawner.js +1225 -77
- package/src/utils/schema-validator.js +5 -0
- package/src/wizard/index.js +3 -2
|
@@ -49,10 +49,10 @@ export function stripLegacyBuildSurface(content) {
|
|
|
49
49
|
const CODEX_CREDENTIAL_BOUNDARY = `
|
|
50
50
|
## Codex external provider credential boundary
|
|
51
51
|
|
|
52
|
-
Commands returned in any \`spawn_command\`, \`
|
|
53
|
-
\`parallel_spawn_commands\` start another provider
|
|
54
|
-
command through its native local execution path so
|
|
55
|
-
existing local credentials:
|
|
52
|
+
Commands returned in any \`spawn_command\`, \`deferred_spawn_command\`,
|
|
53
|
+
\`parallel_spawn_command\` or \`parallel_spawn_commands\` start another provider
|
|
54
|
+
CLI. Codex MUST execute each command through its native local execution path so
|
|
55
|
+
the child CLI can access its existing local credentials:
|
|
56
56
|
|
|
57
57
|
- If the active Codex host requires approval to leave a restricted workspace
|
|
58
58
|
sandbox, request the narrow native approval it exposes for the exact command.
|
package/src/installer/core.js
CHANGED
|
@@ -1,17 +1,18 @@
|
|
|
1
|
-
import { mkdirSync, writeFileSync, copyFileSync, existsSync, readFileSync, readdirSync, statSync, rmSync } from 'fs';
|
|
1
|
+
import { mkdirSync, writeFileSync, copyFileSync, existsSync, readFileSync, readdirSync, statSync, lstatSync, rmSync } from 'fs';
|
|
2
2
|
import { execFileSync } from 'child_process';
|
|
3
3
|
import { join, dirname } from 'path';
|
|
4
4
|
import { fileURLToPath } from 'url';
|
|
5
5
|
import { createRequire } from 'module';
|
|
6
6
|
import { IDE_CONFIGS, IDE_TO_PROVIDER } from '../config/ide-configs.js';
|
|
7
7
|
import { mergeClaudeMCPConfig, upsertManagedMcpToml } from '../config/mcp-configs.js';
|
|
8
|
-
import { generateSessionYaml, generateConfigYaml, generateProjectContext, generateClaudeMd, generateClaudeLocalMd, generateCodexSkill, generateGeminiRouter, generateGrokRouter, generateGeminiSessionLock, generateAgentsOverrideMd, generateCodexConstitutionGuardRules, generateCodexReadProtectionRules } from './templates.js';
|
|
8
|
+
import { generateSessionYaml, generateConfigYaml, generateProjectContext, generateClaudeMd, generateClaudeLocalMd, generateCodexSkill, generateGeminiRouter, generateGrokRouter, generateGrokGovernanceRules, generateGeminiSessionLock, generateAgentsOverrideMd, generateCodexConstitutionGuardRules, generateCodexReadProtectionRules } from './templates.js';
|
|
9
9
|
import { generateContextFiles } from '../config/context-file-generator.js';
|
|
10
10
|
import { adaptFrameworkFile, ADAPTABLE_FILES, stripLegacyBuildSurface } from '../config/framework-adapter.js';
|
|
11
11
|
import { generateClaudeSettings } from '../config/claude-settings-generator.js';
|
|
12
12
|
import { generateProviderOverlays } from './provider-overlay.js';
|
|
13
13
|
import { applyInstalledPathReplacement } from './path-replacement.js';
|
|
14
|
-
import {
|
|
14
|
+
import { verifyFrameworkSource } from './manifest.js';
|
|
15
|
+
import { initializeSessionDocument } from '../orchestrator/session-manager.js';
|
|
15
16
|
import * as yaml from 'js-yaml';
|
|
16
17
|
|
|
17
18
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
@@ -30,47 +31,35 @@ export async function installFramework(config) {
|
|
|
30
31
|
const enabledProviders = allProviders?.length
|
|
31
32
|
? [...new Set(allProviders)]
|
|
32
33
|
: [...new Set((selectedIDEs || []).map(ide => IDE_TO_PROVIDER[ide]).filter(Boolean))];
|
|
33
|
-
//
|
|
34
|
+
// The signed source is always anchored to the package version. `config.version`
|
|
35
|
+
// remains an installer/test presentation override and cannot redefine trust.
|
|
36
|
+
let packageVersion = null;
|
|
37
|
+
try {
|
|
38
|
+
const pkgPath = join(__dirname, '..', '..', 'package.json');
|
|
39
|
+
packageVersion = JSON.parse(readFileSync(pkgPath, 'utf-8')).version;
|
|
40
|
+
} catch { /* the integrity check below will still fail closed on source drift */ }
|
|
41
|
+
|
|
34
42
|
let { version } = config;
|
|
35
|
-
if (!version)
|
|
36
|
-
try {
|
|
37
|
-
const pkgPath = join(__dirname, '..', '..', 'package.json');
|
|
38
|
-
version = JSON.parse(readFileSync(pkgPath, 'utf-8')).version;
|
|
39
|
-
} catch { version = 'unknown'; }
|
|
40
|
-
}
|
|
43
|
+
if (!version) version = packageVersion || 'unknown';
|
|
41
44
|
|
|
42
45
|
// 0. Verify framework signature (supply chain protection)
|
|
43
|
-
const
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
if (existsSync(manifestPath) && existsSync(sigPath)) {
|
|
47
|
-
const manifest = JSON.parse(readFileSync(manifestPath, 'utf-8'));
|
|
48
|
-
const signature = readFileSync(sigPath, 'utf-8').trim();
|
|
49
|
-
const result = verifyManifest(manifest, signature);
|
|
50
|
-
|
|
51
|
-
if (!result.valid && result.reason === 'signature-mismatch') {
|
|
52
|
-
throw new Error('Framework signature verification failed. Package may have been tampered with.');
|
|
53
|
-
}
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
// 1. Create .chati/ session directory
|
|
57
|
-
createDir(join(targetDir, '.chati'));
|
|
58
|
-
const sessionPath = join(targetDir, '.chati', 'session.yaml');
|
|
59
|
-
// Preserve existing session.yaml on reinstall/recovery (don't destroy pipeline state).
|
|
60
|
-
// Only write fresh session if one does not exist.
|
|
61
|
-
if (!existsSync(sessionPath)) {
|
|
62
|
-
writeFileSync(
|
|
63
|
-
sessionPath,
|
|
64
|
-
generateSessionYaml({ projectName, projectType, language, selectedIDEs, selectedMCPs, allProviders: enabledProviders }),
|
|
65
|
-
'utf-8'
|
|
66
|
-
);
|
|
46
|
+
const integrity = verifyFrameworkSource(FRAMEWORK_SOURCE, { expectedVersion: packageVersion });
|
|
47
|
+
if (!integrity.valid) {
|
|
48
|
+
throw new Error(`Framework integrity verification failed (${integrity.reason}). Package may have been tampered with.`);
|
|
67
49
|
}
|
|
50
|
+
// 1. Create the canonical session through the lock-aware session manager.
|
|
51
|
+
// Reinstall/recovery always preserves an existing document.
|
|
52
|
+
const initialSession = initializeSessionDocument(
|
|
53
|
+
targetDir,
|
|
54
|
+
generateSessionYaml({ projectName, projectType, language, selectedIDEs, selectedMCPs, allProviders: enabledProviders }),
|
|
55
|
+
);
|
|
56
|
+
if (initialSession.error) throw new Error(initialSession.error);
|
|
68
57
|
|
|
69
58
|
// 2. Create .chati.dev/ framework directory (hidden — dot prefix)
|
|
70
59
|
// The source is chati.dev/ in the monorepo; installed as .chati.dev/ in user projects.
|
|
71
60
|
// Text replacement during copy handles all internal path references.
|
|
72
61
|
const frameworkDir = join(targetDir, '.chati.dev');
|
|
73
|
-
|
|
62
|
+
createSafeManagedDirectory(frameworkDir);
|
|
74
63
|
|
|
75
64
|
// 2b. Create artifacts/ at PROJECT ROOT (visible to user — this is their output)
|
|
76
65
|
const artifactsDir = join(targetDir, 'artifacts');
|
|
@@ -101,7 +90,7 @@ export async function installFramework(config) {
|
|
|
101
90
|
];
|
|
102
91
|
|
|
103
92
|
for (const dir of frameworkDirs) {
|
|
104
|
-
|
|
93
|
+
createSafeManagedDirectory(join(frameworkDir, dir));
|
|
105
94
|
}
|
|
106
95
|
|
|
107
96
|
// Create .chati/memories/ directory tree for Memory Layer
|
|
@@ -162,6 +151,9 @@ export async function installFramework(config) {
|
|
|
162
151
|
join(__dirname, '..', '..', 'framework', 'hooks'),
|
|
163
152
|
join(frameworkDir, 'framework', 'hooks')
|
|
164
153
|
);
|
|
154
|
+
if (isRuntimeV2) {
|
|
155
|
+
rmSync(join(frameworkDir, 'framework', 'hooks', 'post-dev.js'), { force: true });
|
|
156
|
+
}
|
|
165
157
|
|
|
166
158
|
// Also bundle production deps so the CLI can import js-yaml/chalk/etc. at runtime
|
|
167
159
|
// without relying on a node_modules tree in the user's project.
|
|
@@ -182,7 +174,13 @@ export async function installFramework(config) {
|
|
|
182
174
|
// Write config.yaml
|
|
183
175
|
writeFileSync(
|
|
184
176
|
join(frameworkDir, 'config.yaml'),
|
|
185
|
-
generateConfigYaml({
|
|
177
|
+
generateConfigYaml({
|
|
178
|
+
version, projectType, language, selectedIDEs,
|
|
179
|
+
allProviders: enabledProviders,
|
|
180
|
+
runtimeVersion: config.runtimeVersion,
|
|
181
|
+
installationMode: config.installationMode,
|
|
182
|
+
clickupPreflight: config.clickupPreflight,
|
|
183
|
+
}),
|
|
186
184
|
'utf-8'
|
|
187
185
|
);
|
|
188
186
|
|
|
@@ -324,11 +322,18 @@ function pruneLegacyExecutionSurface(frameworkDir, providers) {
|
|
|
324
322
|
}
|
|
325
323
|
|
|
326
324
|
export function ensureGitRepository(targetDir) {
|
|
325
|
+
const environment = Object.fromEntries(Object.entries(process.env).filter(([key]) => !key.startsWith('GIT_')));
|
|
326
|
+
const gitEnvironment = {
|
|
327
|
+
...environment,
|
|
328
|
+
GIT_CONFIG_NOSYSTEM: '1',
|
|
329
|
+
GIT_CONFIG_GLOBAL: process.platform === 'win32' ? 'NUL' : '/dev/null',
|
|
330
|
+
GIT_TERMINAL_PROMPT: '0',
|
|
331
|
+
};
|
|
327
332
|
try {
|
|
328
|
-
execFileSync('git', ['-C', targetDir, 'rev-parse', '--show-toplevel'], { stdio: 'ignore' });
|
|
333
|
+
execFileSync('git', ['-C', targetDir, 'rev-parse', '--show-toplevel'], { stdio: 'ignore', env: gitEnvironment });
|
|
329
334
|
return { initialized: false };
|
|
330
335
|
} catch {
|
|
331
|
-
execFileSync('git', ['-C', targetDir, 'init', '-q'], { stdio: 'ignore' });
|
|
336
|
+
execFileSync('git', ['-C', targetDir, 'init', '-q'], { stdio: 'ignore', env: gitEnvironment });
|
|
332
337
|
return { initialized: true };
|
|
333
338
|
}
|
|
334
339
|
}
|
|
@@ -395,7 +400,7 @@ const TEXT_EXTENSIONS = new Set(['.md', '.js', '.yaml', '.yml', '.json', '.html'
|
|
|
395
400
|
*/
|
|
396
401
|
function copyDirRecursive(srcDir, destDir, provider, relBase = '') {
|
|
397
402
|
if (!existsSync(srcDir)) return;
|
|
398
|
-
|
|
403
|
+
createSafeManagedDirectory(destDir);
|
|
399
404
|
|
|
400
405
|
const entries = readdirSync(srcDir, { withFileTypes: true });
|
|
401
406
|
for (const entry of entries) {
|
|
@@ -419,10 +424,10 @@ function copyDirRecursive(srcDir, destDir, provider, relBase = '') {
|
|
|
419
424
|
if (provider && ADAPTABLE_FILES.has(relPath)) {
|
|
420
425
|
content = adaptFrameworkFile(content, relPath, provider);
|
|
421
426
|
}
|
|
422
|
-
|
|
427
|
+
writeSafeManagedFile(destPath, content, 'utf-8');
|
|
423
428
|
} else {
|
|
424
429
|
// Binary file: direct copy
|
|
425
|
-
|
|
430
|
+
copySafeManagedFile(srcPath, destPath);
|
|
426
431
|
}
|
|
427
432
|
}
|
|
428
433
|
}
|
|
@@ -445,13 +450,13 @@ export function copyFrameworkFiles(destDir, provider = null) {
|
|
|
445
450
|
const src = join(FRAMEWORK_SOURCE, file);
|
|
446
451
|
if (!existsSync(src)) continue;
|
|
447
452
|
const dest = join(destDir, file);
|
|
448
|
-
|
|
453
|
+
createSafeManagedDirectory(dirname(dest));
|
|
449
454
|
let content = readFileSync(src, 'utf-8');
|
|
450
455
|
content = applyInstalledPathReplacement(content);
|
|
451
456
|
if (provider && ADAPTABLE_FILES.has(file)) {
|
|
452
457
|
content = adaptFrameworkFile(content, file, provider);
|
|
453
458
|
}
|
|
454
|
-
|
|
459
|
+
writeSafeManagedFile(dest, content, 'utf-8');
|
|
455
460
|
}
|
|
456
461
|
|
|
457
462
|
// Copy each framework directory recursively
|
|
@@ -481,7 +486,7 @@ async function configureIDE(targetDir, ideKey, selectedMCPs, options = {}) {
|
|
|
481
486
|
|
|
482
487
|
Run via Bash:
|
|
483
488
|
\`\`\`
|
|
484
|
-
node .chati.dev/orchestrator/chati-router.js
|
|
489
|
+
node .chati.dev/orchestrator/chati-router.js --harness claude
|
|
485
490
|
\`\`\`
|
|
486
491
|
|
|
487
492
|
Parse the JSON output. This single command already handled: license validation, integrity check, session state, pipeline routing, and team registration (if needed). You do NOT need to run any of these separately.
|
|
@@ -497,7 +502,7 @@ Parse the JSON output. This single command already handled: license validation,
|
|
|
497
502
|
|
|
498
503
|
### If \`ok\` is true:
|
|
499
504
|
|
|
500
|
-
1. Use \`session.language\` for
|
|
505
|
+
1. Use \`session.language\` for every visible response, preface, progress message, question and error, starting with the first response (pt = Português, en = English, es = Español, fr = Français).
|
|
501
506
|
2. Read the full orchestrator at \`${orchestratorPath || '.chati.dev/orchestrator/chati.md'}\`.
|
|
502
507
|
3. The \`action\` field tells you what to do. Go to the matching "Action:" section in the orchestrator.
|
|
503
508
|
4. **If \`action\` is \`spawn_team\`**: the team is ALREADY registered (Step 1 already ran as code). The \`team\` object contains \`team_id\`, \`task_list_path\`, \`mailbox_path\`, \`members\`. Go directly to "Action: Spawn Team" Step 2 and use these values in the agent prompts. DO NOT run \`orchestrate spawn-team\` again — it already ran.
|
|
@@ -506,6 +511,14 @@ Parse the JSON output. This single command already handled: license validation,
|
|
|
506
511
|
|
|
507
512
|
**The \`pipeline\` object contains the full JSON from \`orchestrate next\`.** Use its fields (agent_file, spawn_command, handoff_status, etc.) as documented in the orchestrator.
|
|
508
513
|
|
|
514
|
+
**Pending interaction invariant:** If \`action\` is \`resume_pending_interaction\`,
|
|
515
|
+
do not execute a provider command. Display exactly \`pipeline.question\`, with
|
|
516
|
+
no preface, summary or paraphrase. After the user answers, write
|
|
517
|
+
\`{"response":"<exact user response>"}\` to \`pipeline.response_file\` using the
|
|
518
|
+
native file-write tool, never a shell command. Then execute
|
|
519
|
+
\`pipeline.deferred_spawn_command\` exactly once and unchanged. Never execute a
|
|
520
|
+
command merely because a command-shaped field is present.
|
|
521
|
+
|
|
509
522
|
**User input:** $ARGUMENTS
|
|
510
523
|
`;
|
|
511
524
|
writeFileSync(join(targetDir, '.claude', 'commands', 'chati.md'), routerContent, 'utf-8');
|
|
@@ -538,14 +551,14 @@ Parse the JSON output. This single command already handled: license validation,
|
|
|
538
551
|
writeFileSync(join(targetDir, '.codex', 'rules', 'constitution-guard.rules'), generateCodexConstitutionGuardRules(), 'utf-8');
|
|
539
552
|
writeFileSync(join(targetDir, '.codex', 'rules', 'read-protection.rules'), generateCodexReadProtectionRules(), 'utf-8');
|
|
540
553
|
|
|
541
|
-
// .codex/hooks.json —
|
|
542
|
-
//
|
|
543
|
-
//
|
|
544
|
-
//
|
|
554
|
+
// .codex/hooks.json — use Codex's native lifecycle surface for the same
|
|
555
|
+
// controller boundaries enforced in the other enabled harnesses. In
|
|
556
|
+
// particular, `PreToolUse` observes Bash and local mutation tools, not
|
|
557
|
+
// merely the prompt entrypoint. Codex hooks require [features] hooks = true
|
|
558
|
+
// in the active project config.
|
|
545
559
|
writeFileSync(
|
|
546
560
|
join(targetDir, '.codex', 'hooks.json'),
|
|
547
561
|
JSON.stringify({
|
|
548
|
-
$comment: 'chati.dev Codex lifecycle hooks. Requires [features] hooks = true.',
|
|
549
562
|
hooks: {
|
|
550
563
|
UserPromptSubmit: [
|
|
551
564
|
{
|
|
@@ -558,6 +571,34 @@ Parse the JSON output. This single command already handled: license validation,
|
|
|
558
571
|
],
|
|
559
572
|
},
|
|
560
573
|
],
|
|
574
|
+
PreToolUse: [
|
|
575
|
+
{
|
|
576
|
+
matcher: '^Bash$',
|
|
577
|
+
hooks: [
|
|
578
|
+
{
|
|
579
|
+
type: 'command',
|
|
580
|
+
command: 'node .chati.dev/hooks/git-push-authority.js',
|
|
581
|
+
},
|
|
582
|
+
],
|
|
583
|
+
},
|
|
584
|
+
{
|
|
585
|
+
matcher: '^(?:apply_patch|Edit|Write|NotebookEdit)$',
|
|
586
|
+
hooks: [
|
|
587
|
+
{
|
|
588
|
+
type: 'command',
|
|
589
|
+
command: 'node .chati.dev/hooks/mode-governance.js',
|
|
590
|
+
},
|
|
591
|
+
{
|
|
592
|
+
type: 'command',
|
|
593
|
+
command: 'node .chati.dev/hooks/team-quality-gate.js',
|
|
594
|
+
},
|
|
595
|
+
{
|
|
596
|
+
type: 'command',
|
|
597
|
+
command: 'node .chati.dev/hooks/reasoning-escalator.js',
|
|
598
|
+
},
|
|
599
|
+
],
|
|
600
|
+
},
|
|
601
|
+
],
|
|
561
602
|
},
|
|
562
603
|
}, null, 2) + '\n',
|
|
563
604
|
'utf-8'
|
|
@@ -593,6 +634,8 @@ Parse the JSON output. This single command already handled: license validation,
|
|
|
593
634
|
} else if (ideKey === 'grok-cli') {
|
|
594
635
|
writeFileSync(join(targetDir, '.grok', 'commands', 'chati.md'), generateGrokRouter({ orchestratorPath }), 'utf-8');
|
|
595
636
|
writeFileSync(join(targetDir, '.grok', 'session-lock.md'), generateClaudeLocalMd(), 'utf-8');
|
|
637
|
+
createDir(join(targetDir, '.grok', 'rules'));
|
|
638
|
+
writeFileSync(join(targetDir, '.grok', 'rules', 'chati-governance.md'), generateGrokGovernanceRules(), 'utf-8');
|
|
596
639
|
const grokConfigPath = join(targetDir, '.grok', 'config.toml');
|
|
597
640
|
if (selectedMCPs.length > 0 || existsSync(grokConfigPath)) {
|
|
598
641
|
const existingConfig = existsSync(grokConfigPath) ? readFileSync(grokConfigPath, 'utf-8') : '';
|
|
@@ -693,6 +736,7 @@ export function updateGitignore(targetDir, selectedIDEs) {
|
|
|
693
736
|
'',
|
|
694
737
|
'# Runtime state (session, teams, timeline, transient memories)',
|
|
695
738
|
'.chati/session.yaml',
|
|
739
|
+
'.chati/runtime/',
|
|
696
740
|
'.chati/teams/',
|
|
697
741
|
'.chati/timeline.json',
|
|
698
742
|
'.chati/memories/*/session/',
|
|
@@ -739,6 +783,7 @@ export function updateGitignore(targetDir, selectedIDEs) {
|
|
|
739
783
|
'',
|
|
740
784
|
'# Grok provider framework files',
|
|
741
785
|
'.grok/commands/',
|
|
786
|
+
'.grok/rules/',
|
|
742
787
|
'.grok/session-lock.md',
|
|
743
788
|
);
|
|
744
789
|
}
|
|
@@ -955,6 +1000,58 @@ function createDir(dir) {
|
|
|
955
1000
|
}
|
|
956
1001
|
}
|
|
957
1002
|
|
|
1003
|
+
function unsafeManagedPath(path, message) {
|
|
1004
|
+
const error = new Error(`${message}: ${path}`);
|
|
1005
|
+
error.code = 'UNSAFE_INSTALLATION_TARGET';
|
|
1006
|
+
throw error;
|
|
1007
|
+
}
|
|
1008
|
+
|
|
1009
|
+
/** Managed framework paths must never resolve through a user-provided link. */
|
|
1010
|
+
function createSafeManagedDirectory(dir) {
|
|
1011
|
+
const missing = [];
|
|
1012
|
+
let current = join(dir);
|
|
1013
|
+
for (;;) {
|
|
1014
|
+
try {
|
|
1015
|
+
const stat = lstatSync(current);
|
|
1016
|
+
if (stat.isSymbolicLink() || !stat.isDirectory()) unsafeManagedPath(current, 'managed directory must be a real directory');
|
|
1017
|
+
break;
|
|
1018
|
+
} catch (error) {
|
|
1019
|
+
if (error?.code !== 'ENOENT') throw error;
|
|
1020
|
+
missing.push(current);
|
|
1021
|
+
const parent = dirname(current);
|
|
1022
|
+
if (parent === current) unsafeManagedPath(current, 'could not resolve a managed directory ancestor');
|
|
1023
|
+
current = parent;
|
|
1024
|
+
}
|
|
1025
|
+
}
|
|
1026
|
+
for (const path of missing.reverse()) {
|
|
1027
|
+
try { mkdirSync(path); }
|
|
1028
|
+
catch (error) { if (error?.code !== 'EEXIST') throw error; }
|
|
1029
|
+
const stat = lstatSync(path);
|
|
1030
|
+
if (stat.isSymbolicLink() || !stat.isDirectory()) unsafeManagedPath(path, 'managed directory changed while being created');
|
|
1031
|
+
}
|
|
1032
|
+
}
|
|
1033
|
+
|
|
1034
|
+
function assertSafeManagedFileDestination(path) {
|
|
1035
|
+
try {
|
|
1036
|
+
const stat = lstatSync(path);
|
|
1037
|
+
if (stat.isSymbolicLink() || !stat.isFile() || stat.nlink !== 1) {
|
|
1038
|
+
unsafeManagedPath(path, 'managed file destination must be a single-link regular file');
|
|
1039
|
+
}
|
|
1040
|
+
} catch (error) {
|
|
1041
|
+
if (error?.code !== 'ENOENT') throw error;
|
|
1042
|
+
}
|
|
1043
|
+
}
|
|
1044
|
+
|
|
1045
|
+
function writeSafeManagedFile(path, content, encoding) {
|
|
1046
|
+
assertSafeManagedFileDestination(path);
|
|
1047
|
+
writeFileSync(path, content, encoding);
|
|
1048
|
+
}
|
|
1049
|
+
|
|
1050
|
+
function copySafeManagedFile(source, destination) {
|
|
1051
|
+
assertSafeManagedFileDestination(destination);
|
|
1052
|
+
copyFileSync(source, destination);
|
|
1053
|
+
}
|
|
1054
|
+
|
|
958
1055
|
/**
|
|
959
1056
|
* Recursively copy CLI source (excluding tests, node_modules, hidden files).
|
|
960
1057
|
* Used to bundle the CLI source into the user's .chati.dev/_cli/ folder so
|
|
@@ -962,7 +1059,7 @@ function createDir(dir) {
|
|
|
962
1059
|
*/
|
|
963
1060
|
export function copyCliSource(srcDir, destDir) {
|
|
964
1061
|
if (!existsSync(srcDir)) return;
|
|
965
|
-
|
|
1062
|
+
createSafeManagedDirectory(destDir);
|
|
966
1063
|
const entries = readdirSync(srcDir);
|
|
967
1064
|
for (const entry of entries) {
|
|
968
1065
|
if (entry.startsWith('.')) continue;
|
|
@@ -974,7 +1071,7 @@ export function copyCliSource(srcDir, destDir) {
|
|
|
974
1071
|
if (stat.isDirectory()) {
|
|
975
1072
|
copyCliSource(srcPath, destPath);
|
|
976
1073
|
} else {
|
|
977
|
-
|
|
1074
|
+
copySafeManagedFile(srcPath, destPath);
|
|
978
1075
|
}
|
|
979
1076
|
}
|
|
980
1077
|
}
|
|
@@ -984,7 +1081,7 @@ export function copyCliSource(srcDir, destDir) {
|
|
|
984
1081
|
*/
|
|
985
1082
|
function copyDirVerbatim(srcDir, destDir) {
|
|
986
1083
|
if (!existsSync(srcDir)) return;
|
|
987
|
-
|
|
1084
|
+
createSafeManagedDirectory(destDir);
|
|
988
1085
|
for (const entry of readdirSync(srcDir)) {
|
|
989
1086
|
const srcPath = join(srcDir, entry);
|
|
990
1087
|
const destPath = join(destDir, entry);
|
|
@@ -993,7 +1090,11 @@ function copyDirVerbatim(srcDir, destDir) {
|
|
|
993
1090
|
if (stat.isDirectory()) {
|
|
994
1091
|
copyDirVerbatim(srcPath, destPath);
|
|
995
1092
|
} else {
|
|
996
|
-
try {
|
|
1093
|
+
try { copySafeManagedFile(srcPath, destPath); }
|
|
1094
|
+
catch (error) {
|
|
1095
|
+
if (error?.code === 'UNSAFE_INSTALLATION_TARGET') throw error;
|
|
1096
|
+
/* skip unreadable source material */
|
|
1097
|
+
}
|
|
997
1098
|
}
|
|
998
1099
|
}
|
|
999
1100
|
}
|
|
@@ -1021,7 +1122,7 @@ export function copyCliDependencies(pkgDir, destNodeModules) {
|
|
|
1021
1122
|
// Resolve each dep from the package.json location (works for hoisted + nested layouts).
|
|
1022
1123
|
|
|
1023
1124
|
const require = createRequire(join(pkgDir, 'package.json'));
|
|
1024
|
-
|
|
1125
|
+
createSafeManagedDirectory(destNodeModules);
|
|
1025
1126
|
|
|
1026
1127
|
const visited = new Set();
|
|
1027
1128
|
const queue = [...deps];
|
|
@@ -1,5 +1,7 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
1
|
+
import {
|
|
2
|
+
readFileSync, writeFileSync, existsSync, statSync, lstatSync, mkdirSync, readdirSync,
|
|
3
|
+
} from 'fs';
|
|
4
|
+
import { join, dirname, posix } from 'path';
|
|
3
5
|
import { fileURLToPath } from 'url';
|
|
4
6
|
import { verify as cryptoVerify, createPublicKey } from 'crypto';
|
|
5
7
|
import { hashFile } from './file-hasher.js';
|
|
@@ -139,15 +141,144 @@ export function compareManifests(oldManifest, newManifest) {
|
|
|
139
141
|
* @returns {{ valid: boolean, reason: string }}
|
|
140
142
|
*/
|
|
141
143
|
export function verifyManifest(manifest, signatureBase64, publicKeyPem = null) {
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
144
|
+
try {
|
|
145
|
+
// publicKeyPem is a test seam: production callers use the embedded key.
|
|
146
|
+
const key = publicKeyPem ? createPublicKey(publicKeyPem) : SIGNING_PUBLIC_KEY;
|
|
147
|
+
if (!key) return { valid: false, reason: 'no-public-key' };
|
|
148
|
+
if (typeof signatureBase64 !== 'string'
|
|
149
|
+
|| !/^[A-Za-z0-9+/]+={0,2}$/.test(signatureBase64)
|
|
150
|
+
|| Buffer.from(signatureBase64, 'base64').length !== 64) {
|
|
151
|
+
return { valid: false, reason: 'invalid-signature-encoding' };
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
const manifestJson = serializeManifest(manifest);
|
|
155
|
+
const signature = Buffer.from(signatureBase64, 'base64');
|
|
156
|
+
const valid = cryptoVerify(null, Buffer.from(manifestJson), key, signature);
|
|
157
|
+
return { valid, reason: valid ? 'ok' : 'signature-mismatch' };
|
|
158
|
+
} catch {
|
|
159
|
+
return { valid: false, reason: 'manifest-verification-error' };
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
function isCanonicalManifestPath(filePath) {
|
|
164
|
+
return typeof filePath === 'string'
|
|
165
|
+
&& filePath.length > 0
|
|
166
|
+
&& !filePath.includes('\\')
|
|
167
|
+
&& !filePath.includes('\0')
|
|
168
|
+
&& !posix.isAbsolute(filePath)
|
|
169
|
+
&& posix.normalize(filePath) === filePath
|
|
170
|
+
&& filePath !== '.'
|
|
171
|
+
&& !filePath.startsWith('../')
|
|
172
|
+
&& !['manifest.json', 'manifest.sig'].includes(filePath);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
function collectFrameworkFiles(rootDir) {
|
|
176
|
+
const files = new Map();
|
|
177
|
+
|
|
178
|
+
function walk(directory, segments = []) {
|
|
179
|
+
const entries = readdirSync(directory, { withFileTypes: true })
|
|
180
|
+
.sort((left, right) => left.name.localeCompare(right.name));
|
|
181
|
+
for (const entry of entries) {
|
|
182
|
+
if (segments.length === 0 && ['manifest.json', 'manifest.sig'].includes(entry.name)) continue;
|
|
183
|
+
const entrySegments = [...segments, entry.name];
|
|
184
|
+
const relativePath = entrySegments.join('/');
|
|
185
|
+
const absolutePath = join(rootDir, ...entrySegments);
|
|
186
|
+
const stat = lstatSync(absolutePath);
|
|
187
|
+
if (stat.isSymbolicLink()) return { valid: false, reason: 'framework-symlink', file: relativePath };
|
|
188
|
+
if (stat.isDirectory()) {
|
|
189
|
+
const nested = walk(absolutePath, entrySegments);
|
|
190
|
+
if (!nested.valid) return nested;
|
|
191
|
+
} else if (stat.isFile()) {
|
|
192
|
+
if (stat.nlink !== 1) return { valid: false, reason: 'framework-hardlink', file: relativePath };
|
|
193
|
+
files.set(relativePath, { absolutePath, stat });
|
|
194
|
+
} else {
|
|
195
|
+
return { valid: false, reason: 'framework-special-file', file: relativePath };
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
return { valid: true };
|
|
199
|
+
}
|
|
145
200
|
|
|
146
|
-
const
|
|
147
|
-
|
|
201
|
+
const rootStat = lstatSync(rootDir);
|
|
202
|
+
if (rootStat.isSymbolicLink() || !rootStat.isDirectory()) {
|
|
203
|
+
return { valid: false, reason: 'invalid-framework-root', files };
|
|
204
|
+
}
|
|
205
|
+
const result = walk(rootDir);
|
|
206
|
+
return { ...result, files };
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
/**
|
|
210
|
+
* Verify that the signed manifest covers the exact framework file set and
|
|
211
|
+
* that every signed size/hash still matches the bytes about to be installed.
|
|
212
|
+
*/
|
|
213
|
+
export function verifyManifestFiles(rootDir, manifest) {
|
|
214
|
+
if (!manifest || typeof manifest !== 'object' || Array.isArray(manifest)
|
|
215
|
+
|| !manifest.files || typeof manifest.files !== 'object' || Array.isArray(manifest.files)) {
|
|
216
|
+
return { valid: false, reason: 'invalid-manifest-schema' };
|
|
217
|
+
}
|
|
148
218
|
|
|
149
|
-
const
|
|
150
|
-
|
|
219
|
+
const expected = new Map();
|
|
220
|
+
for (const [filePath, record] of Object.entries(manifest.files)) {
|
|
221
|
+
if (!isCanonicalManifestPath(filePath)) {
|
|
222
|
+
return { valid: false, reason: 'unsafe-manifest-path', file: filePath };
|
|
223
|
+
}
|
|
224
|
+
if (!record || typeof record !== 'object' || Array.isArray(record)
|
|
225
|
+
|| !/^[a-f0-9]{64}$/.test(record.hash)
|
|
226
|
+
|| !Number.isSafeInteger(record.size) || record.size < 0
|
|
227
|
+
|| typeof record.type !== 'string') {
|
|
228
|
+
return { valid: false, reason: 'invalid-manifest-record', file: filePath };
|
|
229
|
+
}
|
|
230
|
+
expected.set(filePath, record);
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
let collected;
|
|
234
|
+
try { collected = collectFrameworkFiles(rootDir); }
|
|
235
|
+
catch (error) {
|
|
236
|
+
return { valid: false, reason: 'framework-read-error', detail: error.message };
|
|
237
|
+
}
|
|
238
|
+
if (!collected.valid) return collected;
|
|
239
|
+
|
|
240
|
+
for (const filePath of expected.keys()) {
|
|
241
|
+
if (!collected.files.has(filePath)) return { valid: false, reason: 'manifest-file-missing', file: filePath };
|
|
242
|
+
}
|
|
243
|
+
for (const filePath of collected.files.keys()) {
|
|
244
|
+
if (!expected.has(filePath)) return { valid: false, reason: 'unsigned-framework-file', file: filePath };
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
for (const [filePath, record] of expected) {
|
|
248
|
+
const actual = collected.files.get(filePath);
|
|
249
|
+
if (actual.stat.size !== record.size) return { valid: false, reason: 'framework-size-mismatch', file: filePath };
|
|
250
|
+
try {
|
|
251
|
+
if (hashFile(actual.absolutePath) !== record.hash) {
|
|
252
|
+
return { valid: false, reason: 'framework-hash-mismatch', file: filePath };
|
|
253
|
+
}
|
|
254
|
+
} catch (error) {
|
|
255
|
+
return { valid: false, reason: 'framework-read-error', file: filePath, detail: error.message };
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
return { valid: true, reason: 'ok', fileCount: expected.size };
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
/** Verify the complete signed framework source before the installer writes. */
|
|
262
|
+
export function verifyFrameworkSource(rootDir, { publicKeyPem = null, expectedVersion = null } = {}) {
|
|
263
|
+
const manifestPath = join(rootDir, 'manifest.json');
|
|
264
|
+
const signaturePath = join(rootDir, 'manifest.sig');
|
|
265
|
+
if (!existsSync(manifestPath)) return { valid: false, reason: 'manifest-missing' };
|
|
266
|
+
if (!existsSync(signaturePath)) return { valid: false, reason: 'signature-missing' };
|
|
267
|
+
|
|
268
|
+
let manifest;
|
|
269
|
+
let signature;
|
|
270
|
+
try {
|
|
271
|
+
manifest = JSON.parse(readFileSync(manifestPath, 'utf8'));
|
|
272
|
+
signature = readFileSync(signaturePath, 'utf8').trim();
|
|
273
|
+
} catch (error) {
|
|
274
|
+
return { valid: false, reason: 'manifest-read-error', detail: error.message };
|
|
275
|
+
}
|
|
276
|
+
const signatureResult = verifyManifest(manifest, signature, publicKeyPem);
|
|
277
|
+
if (!signatureResult.valid) return signatureResult;
|
|
278
|
+
if (expectedVersion !== null && manifest.version !== expectedVersion) {
|
|
279
|
+
return { valid: false, reason: 'manifest-version-mismatch' };
|
|
280
|
+
}
|
|
281
|
+
return verifyManifestFiles(rootDir, manifest);
|
|
151
282
|
}
|
|
152
283
|
|
|
153
284
|
/**
|