chati-dev 4.5.16 → 4.5.28
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 +1 -1
- 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 +252 -25
- package/framework/orchestrator/chati.md +238 -70
- package/framework/schemas/session.schema.json +1 -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 +148 -53
- package/src/installer/manifest.js +140 -9
- package/src/installer/package-artifact.js +249 -0
- package/src/installer/templates.js +65 -20
- 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 +247 -0
- package/src/installer-v2/wizard-installation.js +1 -1
- package/src/orchestrator/browser-runtime.js +44 -13
- package/src/orchestrator/cli.js +1545 -174
- package/src/orchestrator/clickup-projection.js +43 -1
- package/src/orchestrator/clickup-runtime.js +355 -39
- package/src/orchestrator/doctor.js +16 -3
- 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 +1167 -53
- 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 +9 -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 +384 -24
- 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/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.
|
|
@@ -330,11 +322,18 @@ function pruneLegacyExecutionSurface(frameworkDir, providers) {
|
|
|
330
322
|
}
|
|
331
323
|
|
|
332
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
|
+
};
|
|
333
332
|
try {
|
|
334
|
-
execFileSync('git', ['-C', targetDir, 'rev-parse', '--show-toplevel'], { stdio: 'ignore' });
|
|
333
|
+
execFileSync('git', ['-C', targetDir, 'rev-parse', '--show-toplevel'], { stdio: 'ignore', env: gitEnvironment });
|
|
335
334
|
return { initialized: false };
|
|
336
335
|
} catch {
|
|
337
|
-
execFileSync('git', ['-C', targetDir, 'init', '-q'], { stdio: 'ignore' });
|
|
336
|
+
execFileSync('git', ['-C', targetDir, 'init', '-q'], { stdio: 'ignore', env: gitEnvironment });
|
|
338
337
|
return { initialized: true };
|
|
339
338
|
}
|
|
340
339
|
}
|
|
@@ -401,7 +400,7 @@ const TEXT_EXTENSIONS = new Set(['.md', '.js', '.yaml', '.yml', '.json', '.html'
|
|
|
401
400
|
*/
|
|
402
401
|
function copyDirRecursive(srcDir, destDir, provider, relBase = '') {
|
|
403
402
|
if (!existsSync(srcDir)) return;
|
|
404
|
-
|
|
403
|
+
createSafeManagedDirectory(destDir);
|
|
405
404
|
|
|
406
405
|
const entries = readdirSync(srcDir, { withFileTypes: true });
|
|
407
406
|
for (const entry of entries) {
|
|
@@ -425,10 +424,10 @@ function copyDirRecursive(srcDir, destDir, provider, relBase = '') {
|
|
|
425
424
|
if (provider && ADAPTABLE_FILES.has(relPath)) {
|
|
426
425
|
content = adaptFrameworkFile(content, relPath, provider);
|
|
427
426
|
}
|
|
428
|
-
|
|
427
|
+
writeSafeManagedFile(destPath, content, 'utf-8');
|
|
429
428
|
} else {
|
|
430
429
|
// Binary file: direct copy
|
|
431
|
-
|
|
430
|
+
copySafeManagedFile(srcPath, destPath);
|
|
432
431
|
}
|
|
433
432
|
}
|
|
434
433
|
}
|
|
@@ -451,13 +450,13 @@ export function copyFrameworkFiles(destDir, provider = null) {
|
|
|
451
450
|
const src = join(FRAMEWORK_SOURCE, file);
|
|
452
451
|
if (!existsSync(src)) continue;
|
|
453
452
|
const dest = join(destDir, file);
|
|
454
|
-
|
|
453
|
+
createSafeManagedDirectory(dirname(dest));
|
|
455
454
|
let content = readFileSync(src, 'utf-8');
|
|
456
455
|
content = applyInstalledPathReplacement(content);
|
|
457
456
|
if (provider && ADAPTABLE_FILES.has(file)) {
|
|
458
457
|
content = adaptFrameworkFile(content, file, provider);
|
|
459
458
|
}
|
|
460
|
-
|
|
459
|
+
writeSafeManagedFile(dest, content, 'utf-8');
|
|
461
460
|
}
|
|
462
461
|
|
|
463
462
|
// Copy each framework directory recursively
|
|
@@ -512,6 +511,14 @@ Parse the JSON output. This single command already handled: license validation,
|
|
|
512
511
|
|
|
513
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.
|
|
514
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
|
+
|
|
515
522
|
**User input:** $ARGUMENTS
|
|
516
523
|
`;
|
|
517
524
|
writeFileSync(join(targetDir, '.claude', 'commands', 'chati.md'), routerContent, 'utf-8');
|
|
@@ -544,14 +551,14 @@ Parse the JSON output. This single command already handled: license validation,
|
|
|
544
551
|
writeFileSync(join(targetDir, '.codex', 'rules', 'constitution-guard.rules'), generateCodexConstitutionGuardRules(), 'utf-8');
|
|
545
552
|
writeFileSync(join(targetDir, '.codex', 'rules', 'read-protection.rules'), generateCodexReadProtectionRules(), 'utf-8');
|
|
546
553
|
|
|
547
|
-
// .codex/hooks.json —
|
|
548
|
-
//
|
|
549
|
-
//
|
|
550
|
-
//
|
|
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.
|
|
551
559
|
writeFileSync(
|
|
552
560
|
join(targetDir, '.codex', 'hooks.json'),
|
|
553
561
|
JSON.stringify({
|
|
554
|
-
$comment: 'chati.dev Codex lifecycle hooks. Requires [features] hooks = true.',
|
|
555
562
|
hooks: {
|
|
556
563
|
UserPromptSubmit: [
|
|
557
564
|
{
|
|
@@ -564,6 +571,34 @@ Parse the JSON output. This single command already handled: license validation,
|
|
|
564
571
|
],
|
|
565
572
|
},
|
|
566
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
|
+
],
|
|
567
602
|
},
|
|
568
603
|
}, null, 2) + '\n',
|
|
569
604
|
'utf-8'
|
|
@@ -599,6 +634,8 @@ Parse the JSON output. This single command already handled: license validation,
|
|
|
599
634
|
} else if (ideKey === 'grok-cli') {
|
|
600
635
|
writeFileSync(join(targetDir, '.grok', 'commands', 'chati.md'), generateGrokRouter({ orchestratorPath }), 'utf-8');
|
|
601
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');
|
|
602
639
|
const grokConfigPath = join(targetDir, '.grok', 'config.toml');
|
|
603
640
|
if (selectedMCPs.length > 0 || existsSync(grokConfigPath)) {
|
|
604
641
|
const existingConfig = existsSync(grokConfigPath) ? readFileSync(grokConfigPath, 'utf-8') : '';
|
|
@@ -699,6 +736,7 @@ export function updateGitignore(targetDir, selectedIDEs) {
|
|
|
699
736
|
'',
|
|
700
737
|
'# Runtime state (session, teams, timeline, transient memories)',
|
|
701
738
|
'.chati/session.yaml',
|
|
739
|
+
'.chati/runtime/',
|
|
702
740
|
'.chati/teams/',
|
|
703
741
|
'.chati/timeline.json',
|
|
704
742
|
'.chati/memories/*/session/',
|
|
@@ -745,6 +783,7 @@ export function updateGitignore(targetDir, selectedIDEs) {
|
|
|
745
783
|
'',
|
|
746
784
|
'# Grok provider framework files',
|
|
747
785
|
'.grok/commands/',
|
|
786
|
+
'.grok/rules/',
|
|
748
787
|
'.grok/session-lock.md',
|
|
749
788
|
);
|
|
750
789
|
}
|
|
@@ -961,6 +1000,58 @@ function createDir(dir) {
|
|
|
961
1000
|
}
|
|
962
1001
|
}
|
|
963
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
|
+
|
|
964
1055
|
/**
|
|
965
1056
|
* Recursively copy CLI source (excluding tests, node_modules, hidden files).
|
|
966
1057
|
* Used to bundle the CLI source into the user's .chati.dev/_cli/ folder so
|
|
@@ -968,7 +1059,7 @@ function createDir(dir) {
|
|
|
968
1059
|
*/
|
|
969
1060
|
export function copyCliSource(srcDir, destDir) {
|
|
970
1061
|
if (!existsSync(srcDir)) return;
|
|
971
|
-
|
|
1062
|
+
createSafeManagedDirectory(destDir);
|
|
972
1063
|
const entries = readdirSync(srcDir);
|
|
973
1064
|
for (const entry of entries) {
|
|
974
1065
|
if (entry.startsWith('.')) continue;
|
|
@@ -980,7 +1071,7 @@ export function copyCliSource(srcDir, destDir) {
|
|
|
980
1071
|
if (stat.isDirectory()) {
|
|
981
1072
|
copyCliSource(srcPath, destPath);
|
|
982
1073
|
} else {
|
|
983
|
-
|
|
1074
|
+
copySafeManagedFile(srcPath, destPath);
|
|
984
1075
|
}
|
|
985
1076
|
}
|
|
986
1077
|
}
|
|
@@ -990,7 +1081,7 @@ export function copyCliSource(srcDir, destDir) {
|
|
|
990
1081
|
*/
|
|
991
1082
|
function copyDirVerbatim(srcDir, destDir) {
|
|
992
1083
|
if (!existsSync(srcDir)) return;
|
|
993
|
-
|
|
1084
|
+
createSafeManagedDirectory(destDir);
|
|
994
1085
|
for (const entry of readdirSync(srcDir)) {
|
|
995
1086
|
const srcPath = join(srcDir, entry);
|
|
996
1087
|
const destPath = join(destDir, entry);
|
|
@@ -999,7 +1090,11 @@ function copyDirVerbatim(srcDir, destDir) {
|
|
|
999
1090
|
if (stat.isDirectory()) {
|
|
1000
1091
|
copyDirVerbatim(srcPath, destPath);
|
|
1001
1092
|
} else {
|
|
1002
|
-
try {
|
|
1093
|
+
try { copySafeManagedFile(srcPath, destPath); }
|
|
1094
|
+
catch (error) {
|
|
1095
|
+
if (error?.code === 'UNSAFE_INSTALLATION_TARGET') throw error;
|
|
1096
|
+
/* skip unreadable source material */
|
|
1097
|
+
}
|
|
1003
1098
|
}
|
|
1004
1099
|
}
|
|
1005
1100
|
}
|
|
@@ -1027,7 +1122,7 @@ export function copyCliDependencies(pkgDir, destNodeModules) {
|
|
|
1027
1122
|
// Resolve each dep from the package.json location (works for hoisted + nested layouts).
|
|
1028
1123
|
|
|
1029
1124
|
const require = createRequire(join(pkgDir, 'package.json'));
|
|
1030
|
-
|
|
1125
|
+
createSafeManagedDirectory(destNodeModules);
|
|
1031
1126
|
|
|
1032
1127
|
const visited = new Set();
|
|
1033
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
|
/**
|