chati-dev 4.2.0 → 4.2.2
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/framework/config.yaml +3 -3
- package/framework/constitution.md +3 -1
- package/framework/context/governance.md +24 -1
- package/framework/context/quality.md +14 -1
- package/framework/context/root.md +4 -3
- package/framework/hooks/constitution-guard.js +69 -33
- package/framework/hooks/license-guard.js +92 -188
- package/framework/hooks/mode-governance.js +55 -14
- package/framework/hooks/model-governance.js +18 -8
- package/framework/hooks/package.json +3 -0
- package/framework/hooks/prism-engine.js +22 -8
- package/framework/hooks/read-protection.js +37 -9
- package/framework/hooks/session-digest.js +45 -20
- package/framework/hooks/style-guard.js +30 -10
- package/framework/hooks/team-quality-gate.js +39 -13
- package/framework/hooks/undercover-guard.js +30 -11
- package/framework/orchestrator/chati.md +31 -28
- package/package.json +1 -1
- package/scripts/validate-package.js +256 -8
- package/src/config/claude-settings-generator.js +206 -0
- package/src/config/gemini-hooks-generator.js +58 -0
- package/src/installer/core.js +223 -129
- package/src/installer/templates.js +38 -0
- package/src/orchestrator/cli.js +41 -11
package/framework/config.yaml
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
# chati.dev Configuration
|
|
2
|
-
version: "4.2.
|
|
2
|
+
version: "4.2.2"
|
|
3
3
|
installed_at: "2026-02-07T10:00:00Z"
|
|
4
4
|
updated_at: "2026-04-11T00:00:00Z"
|
|
5
|
-
installer_version: "4.2.
|
|
5
|
+
installer_version: "4.2.2"
|
|
6
6
|
project_type: greenfield
|
|
7
7
|
language: en
|
|
8
8
|
ides: [claude-code]
|
|
@@ -54,7 +54,7 @@ features:
|
|
|
54
54
|
frustration_detection: true # Detect user frustration and adapt response style
|
|
55
55
|
bash_security_checks: true # 23-point shell injection defense system
|
|
56
56
|
# Agent Teams (v4.2.0 — Article XXI)
|
|
57
|
-
agent_teams:
|
|
57
|
+
agent_teams: true # Enable Claude Code native Agent Teams. Gated to claude provider; Gemini/Codex fall back silently.
|
|
58
58
|
team_planning_size: 3 # Planning Team max teammates: detail + architect + ux
|
|
59
59
|
team_build_size: 2 # Build Team max teammates: dev + qa-implementation
|
|
60
60
|
team_echo_threshold: 0.92 # Content similarity ratio to trigger Echo Detection (Article XXII)
|
|
@@ -755,6 +755,8 @@ If any check is NO, the compliance block itself is an ERROR and the report canno
|
|
|
755
755
|
|
|
756
756
|
---
|
|
757
757
|
|
|
758
|
-
*Chati.dev Constitution v4.2.
|
|
758
|
+
*Chati.dev Constitution v4.2.2 — 22 Articles + Preamble*
|
|
759
759
|
*v4.2.0 Amendments: Article V amended (Team Communication); Article VIII amended (Team Handoff Envelope); Article XII amended (PRISM L6 Team Roster); Article XXI added (Agent Teams Governance); Article XXII added (Fault Vector Protocol)*
|
|
760
|
+
*v4.2.1 Fixes: installer refactor (recursive copy), features block in config, context files sync*
|
|
761
|
+
*v4.2.2 Fixes: 10 hooks rewritten to canonical hookSpecificOutput schema; .claude/settings.json now actually written by installer (CRITICAL — hooks were dormant before); license enforcement extended to Gemini and Codex per-turn; Agent Teams default ON; orchestrator jargon removed from user-facing output*
|
|
760
762
|
*All agents are bound by this Constitution. Violations are enforced per article.*
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Chati.dev Governance Rules
|
|
2
2
|
|
|
3
|
-
Extracted from `chati.dev/constitution.md` (
|
|
3
|
+
Extracted from `chati.dev/constitution.md` (22 Articles). Read the full constitution for complete rules.
|
|
4
4
|
|
|
5
5
|
## Mode Governance (Article XI)
|
|
6
6
|
- **3 modes**: planning (read all, write chati.dev/ only), build (full access), deploy (full + infra)
|
|
@@ -50,3 +50,26 @@ Extracted from `chati.dev/constitution.md` (19 Articles). Read the full constitu
|
|
|
50
50
|
- Provider selected at install time, auto-configures optimal models per agent
|
|
51
51
|
- Resolution chain: agent_overrides > agent default > primary provider
|
|
52
52
|
- Context files auto-generated for non-Claude providers (GEMINI.md, AGENTS.md)
|
|
53
|
+
|
|
54
|
+
## Anti-Loop Protocol (Article XX)
|
|
55
|
+
- 3-Strike Rule: 3 edits to same file without resolution = STOP
|
|
56
|
+
- Mandatory Diagnosis Mode: read complete file, trace path, root cause in one sentence
|
|
57
|
+
- Paradigm Change required after diagnosis (CSS failed → try JavaScript, etc.)
|
|
58
|
+
- Escalation after 5 total attempts: present honestly, ask for direction
|
|
59
|
+
- Task score capped at 0% for violations
|
|
60
|
+
|
|
61
|
+
## Agent Teams Governance (Article XXI)
|
|
62
|
+
- Feature-flagged (`agent_teams: true` in config.yaml), defaults to OFF
|
|
63
|
+
- 2 canonical teams: Planning Team (Detail+Architect+UX) and Build Team (Dev+QA)
|
|
64
|
+
- Only orchestrator can form/dissolve teams
|
|
65
|
+
- Intra-team communication via Team Mailbox (Article V amendment)
|
|
66
|
+
- Cross-team communication still orchestrator-mediated
|
|
67
|
+
- Team Quality Gate: all tasks complete + scores pass + mailbox clean
|
|
68
|
+
- Max 2 Team Correction Cycles before degraded dissolution
|
|
69
|
+
- Graceful fallback to sequential pipeline when unavailable (Gemini/Codex/flag off)
|
|
70
|
+
|
|
71
|
+
## Fault Vector Protocol (Article XXII)
|
|
72
|
+
- Root Layer Routing: classify fault origin (INTENT/SPEC/CODE/DEFER) before correction
|
|
73
|
+
- Evidence-Bound Verdicts: ERROR findings require tool evidence (lint/typecheck/test/screenshot)
|
|
74
|
+
- Decision Trail: append-only log of known-bad states to prevent cyclical re-introduction
|
|
75
|
+
- Echo Detection: content-similarity matching to detect same defect reappearing across review cycles
|
|
@@ -5,7 +5,8 @@
|
|
|
5
5
|
|-------|--------------|
|
|
6
6
|
| qa-planning | 95% |
|
|
7
7
|
| qa-implementation | 95% |
|
|
8
|
-
|
|
|
8
|
+
| Standard agents | 90% |
|
|
9
|
+
| Discovery agents (WU, brief) | 85% |
|
|
9
10
|
|
|
10
11
|
## Review Range
|
|
11
12
|
Scores within 5 points below threshold trigger REVIEW (human confirmation required even in autonomous mode).
|
|
@@ -26,3 +27,15 @@ Scores within 5 points below threshold trigger REVIEW (human confirmation requir
|
|
|
26
27
|
## Blocker Taxonomy
|
|
27
28
|
- **C01-C15**: Code blockers (technical)
|
|
28
29
|
- **G01-G08**: General blockers (process, communication, dependency)
|
|
30
|
+
|
|
31
|
+
## Fault Vector Protocol (Article XXII)
|
|
32
|
+
- **Root Layer Routing**: INTENT (requirement wrong) -> Brief/Detail | SPEC (spec wrong) -> Architect/Tasks | CODE (implementation wrong) -> Dev | DEFER (out of scope) -> Backlog
|
|
33
|
+
- **Evidence-Bound Verdicts**: ERROR/WARNING require tool evidence. Without evidence -> downgraded to SUGGESTION (non-blocking)
|
|
34
|
+
- **Decision Trail**: Known-bad states logged in session.yaml `decision_trail[]`. Agents read before iterating.
|
|
35
|
+
- **Echo Detection**: Similarity >= 0.85 with prior Decision Trail entry = ECHO. Escalate immediately, no more automated loops.
|
|
36
|
+
- **FVP Compliance Checklist**: Mandatory in every QA correction report
|
|
37
|
+
|
|
38
|
+
## Team Quality Gate (Article XXI)
|
|
39
|
+
- All team tasks complete + all scores pass tier threshold + mailbox clean
|
|
40
|
+
- Max 2 Team Correction Cycles before degraded dissolution
|
|
41
|
+
- Team score = average of member task scores
|
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
# Chati.dev System Context
|
|
2
2
|
|
|
3
3
|
## Framework
|
|
4
|
-
- **Version**: 4.
|
|
4
|
+
- **Version**: 4.2.2
|
|
5
5
|
- **Agents**: Specialized agents across DISCOVER, PLAN, BUILD, DEPLOY phases
|
|
6
|
-
- **Constitution**:
|
|
7
|
-
- **Quality**: 5 pipeline gates + 3-tier verdicts
|
|
6
|
+
- **Constitution**: 22 Articles + Preamble
|
|
7
|
+
- **Quality**: 5 pipeline gates + 3-tier verdicts + Fault Vector Protocol (Article XXII)
|
|
8
|
+
- **Agent Teams**: Native Claude Code teams with peer communication (Article XXI, default ON in v4.2.2 for Claude provider)
|
|
8
9
|
|
|
9
10
|
## Key References
|
|
10
11
|
- **Session State**: `.chati/session.yaml` (runtime — not committed)
|
|
@@ -81,10 +81,9 @@ async function main() {
|
|
|
81
81
|
const secrets = containsSecrets(content);
|
|
82
82
|
|
|
83
83
|
if (secrets.length > 0) {
|
|
84
|
-
process.stdout.write(JSON.stringify(
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
}));
|
|
84
|
+
process.stdout.write(JSON.stringify(denyOutput(
|
|
85
|
+
`[Article IV] Potential secret detected in file content. Pattern: ${secrets[0]}. Use environment variables instead.`
|
|
86
|
+
)));
|
|
88
87
|
return;
|
|
89
88
|
}
|
|
90
89
|
}
|
|
@@ -93,10 +92,9 @@ async function main() {
|
|
|
93
92
|
if (toolName === 'Bash') {
|
|
94
93
|
const command = toolInput.command || '';
|
|
95
94
|
if (isDestructiveCommand(command)) {
|
|
96
|
-
process.stdout.write(JSON.stringify(
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
}));
|
|
95
|
+
process.stdout.write(JSON.stringify(denyOutput(
|
|
96
|
+
`[Article IV] Destructive command detected: "${command.slice(0, 60)}...". This requires explicit user confirmation.`
|
|
97
|
+
)));
|
|
100
98
|
return;
|
|
101
99
|
}
|
|
102
100
|
|
|
@@ -105,28 +103,49 @@ async function main() {
|
|
|
105
103
|
if (injectionFindings.length > 0) {
|
|
106
104
|
const critical = injectionFindings.filter(f => f.severity === 'critical');
|
|
107
105
|
if (critical.length > 0) {
|
|
108
|
-
process.stdout.write(JSON.stringify(
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
}));
|
|
106
|
+
process.stdout.write(JSON.stringify(denyOutput(
|
|
107
|
+
`[Article IV] Shell injection risk detected (${critical.length} critical): ${critical.map(f => f.id).join(', ')}. Command: "${command.slice(0, 60)}..."`
|
|
108
|
+
)));
|
|
112
109
|
return;
|
|
113
110
|
}
|
|
114
|
-
// High/medium findings: allow
|
|
115
|
-
process.stdout.write(JSON.stringify(
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
}));
|
|
111
|
+
// High/medium findings: allow (advisory only, surfaced via reason)
|
|
112
|
+
process.stdout.write(JSON.stringify(allowOutput(
|
|
113
|
+
`[Security Advisory] ${injectionFindings.length} shell security finding(s): ${injectionFindings.map(f => f.id).join(', ')}`
|
|
114
|
+
)));
|
|
119
115
|
return;
|
|
120
116
|
}
|
|
121
117
|
}
|
|
122
118
|
|
|
123
|
-
process.stdout.write(JSON.stringify(
|
|
119
|
+
process.stdout.write(JSON.stringify(allowOutput()));
|
|
124
120
|
} catch (err) {
|
|
125
121
|
process.stderr.write(`[chati-hook-error] constitution-guard: ${err?.message || 'unknown'}\n`);
|
|
126
|
-
|
|
122
|
+
// Fail-open on hook errors: a broken hook should not block legitimate work.
|
|
123
|
+
// Real security checks above are explicit and run before this catch.
|
|
124
|
+
process.stdout.write(JSON.stringify(allowOutput()));
|
|
127
125
|
}
|
|
128
126
|
}
|
|
129
127
|
|
|
128
|
+
function allowOutput(reason) {
|
|
129
|
+
const out = {
|
|
130
|
+
hookSpecificOutput: {
|
|
131
|
+
hookEventName: 'PreToolUse',
|
|
132
|
+
permissionDecision: 'allow',
|
|
133
|
+
},
|
|
134
|
+
};
|
|
135
|
+
if (reason) out.hookSpecificOutput.permissionDecisionReason = reason;
|
|
136
|
+
return out;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
function denyOutput(reason) {
|
|
140
|
+
return {
|
|
141
|
+
hookSpecificOutput: {
|
|
142
|
+
hookEventName: 'PreToolUse',
|
|
143
|
+
permissionDecision: 'deny',
|
|
144
|
+
permissionDecisionReason: reason,
|
|
145
|
+
},
|
|
146
|
+
};
|
|
147
|
+
}
|
|
148
|
+
|
|
130
149
|
/**
|
|
131
150
|
* 23-point shell injection security checks.
|
|
132
151
|
* Inline implementation (hooks are standalone — no imports from src/).
|
|
@@ -135,15 +154,24 @@ async function main() {
|
|
|
135
154
|
// SYNC: These 23 check IDs MUST match packages/chati-dev/src/security/bash-security.js
|
|
136
155
|
// Hooks are standalone (no imports from src/), so duplication is necessary.
|
|
137
156
|
// If you update here, update bash-security.js too. Run: npm test to verify both.
|
|
157
|
+
// Severity tiers:
|
|
158
|
+
// - critical = block immediately (clearly malicious, no legitimate use)
|
|
159
|
+
// - high/medium = advisory only (surfaced as reason, allowed). Used to be
|
|
160
|
+
// blocking, but several patterns produced too many false positives
|
|
161
|
+
// (e.g. SHELL_METACHARACTERS firing on every $(date), GIT_COMMIT_SUBSTITUTION
|
|
162
|
+
// firing on git commit -m "$(date)"). Critical patterns below are narrowly
|
|
163
|
+
// scoped to actually-dangerous shell injection vectors.
|
|
138
164
|
const SHELL_INJECTION_CHECKS = [
|
|
139
|
-
{ id: 'INCOMPLETE_COMMANDS', pattern: /[|&;]\s*$/, severity: '
|
|
165
|
+
{ id: 'INCOMPLETE_COMMANDS', pattern: /[|&;]\s*$/, severity: 'medium' },
|
|
140
166
|
{ id: 'JQ_SYSTEM_FUNCTION', pattern: /jq\b.*\bsystem\s*\(/i, severity: 'critical' },
|
|
141
|
-
{ id: 'JQ_FILE_ARGUMENTS', pattern: /jq\b.*--from-file|jq\b.*-f\s+[^|&;]+/i, severity: '
|
|
142
|
-
{ id: 'OBFUSCATED_FLAGS', pattern: /\$[({].*[)}].*-/, severity: '
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
{ id: '
|
|
146
|
-
|
|
167
|
+
{ id: 'JQ_FILE_ARGUMENTS', pattern: /jq\b.*--from-file|jq\b.*-f\s+[^|&;]+/i, severity: 'medium' },
|
|
168
|
+
{ id: 'OBFUSCATED_FLAGS', pattern: /\$[({].*[)}].*-/, severity: 'medium' },
|
|
169
|
+
// SHELL_METACHARACTERS removed: $() and backticks are normal substitution.
|
|
170
|
+
// Genuinely dangerous substitutions are caught by DANGEROUS_PATTERNS_COMMAND_SUBSTITUTION below.
|
|
171
|
+
{ id: 'DANGEROUS_VARIABLES', pattern: /(?:^|\s)(?:LD_PRELOAD|LD_LIBRARY_PATH|DYLD_INSERT_LIBRARIES|PYTHONPATH|NODE_PATH|RUBYLIB|PERL5LIB)\s*=/, severity: 'critical' },
|
|
172
|
+
// PATH= removed from critical (legitimate `PATH=/foo:$PATH cmd` exists).
|
|
173
|
+
{ id: 'NEWLINES', pattern: /(?<!\\)\n.*(?:rm|curl|wget|chmod|chown|sudo|eval|exec)/, severity: 'medium' },
|
|
174
|
+
{ id: 'BACKSLASH_ESCAPED_WHITESPACE', pattern: /\\\s+(?:-|\/)/, severity: 'medium' },
|
|
147
175
|
// eslint-disable-next-line no-control-regex
|
|
148
176
|
{ id: 'CONTROL_CHARACTERS', pattern: /[\x00-\x08\x0e-\x1f\x7f]/, severity: 'critical' },
|
|
149
177
|
{ id: 'UNICODE_WHITESPACE', pattern: /[\u200B-\u200F\u2028-\u202F\uFEFF\u00A0\u2060\u180E]/, severity: 'critical' },
|
|
@@ -151,13 +179,13 @@ const SHELL_INJECTION_CHECKS = [
|
|
|
151
179
|
{ id: 'DANGEROUS_PATTERNS_INPUT_REDIRECTION', pattern: /<\s*(?:\/etc\/(?:passwd|shadow|sudoers)|\/proc\/|~\/\.ssh\/|~\/\.aws\/)/, severity: 'critical' },
|
|
152
180
|
{ id: 'DANGEROUS_PATTERNS_OUTPUT_REDIRECTION', pattern: />\s*(?:\/etc\/|~\/\.ssh\/|~\/\.bashrc|~\/\.zshrc|~\/\.profile|~\/\.gitconfig)/, severity: 'critical' },
|
|
153
181
|
{ id: 'IFS_INJECTION', pattern: /\bIFS\s*=/, severity: 'critical' },
|
|
154
|
-
{ id: 'BRACE_EXPANSION', pattern: /\{.*(?:rm|curl|wget|chmod|eval|exec|sudo).*[,}]/, severity: '
|
|
155
|
-
|
|
182
|
+
{ id: 'BRACE_EXPANSION', pattern: /\{.*(?:rm|curl|wget|chmod|eval|exec|sudo).*[,}]/, severity: 'medium' },
|
|
183
|
+
// GIT_COMMIT_SUBSTITUTION removed: `git commit -m "$(date)"` is legitimate.
|
|
156
184
|
{ id: 'PROC_ENVIRON_ACCESS', pattern: /\/proc\/(?:self|\d+)\/(?:environ|cmdline|maps|mem)/, severity: 'critical' },
|
|
157
|
-
{ id: 'MALFORMED_TOKEN_INJECTION', pattern: /\\x[0-9a-f]{2}|\\u[0-9a-f]{4}|\\[0-7]{3}/i, severity: '
|
|
185
|
+
{ id: 'MALFORMED_TOKEN_INJECTION', pattern: /\\x[0-9a-f]{2}|\\u[0-9a-f]{4}|\\[0-7]{3}/i, severity: 'medium' },
|
|
158
186
|
{ id: 'MID_WORD_HASH', pattern: /\w#\w/, severity: 'medium' },
|
|
159
187
|
{ id: 'COMMENT_QUOTE_DESYNC', pattern: /#.*['"][^'"]*$/, severity: 'medium' },
|
|
160
|
-
{ id: 'QUOTED_NEWLINE', pattern: /["'][^"']*\n[^"']*["']/, severity: '
|
|
188
|
+
{ id: 'QUOTED_NEWLINE', pattern: /["'][^"']*\n[^"']*["']/, severity: 'medium' },
|
|
161
189
|
{ id: 'ZSH_DANGEROUS_COMMANDS', pattern: /\b(?:zmodload|sysopen|sysread|syswrite|zsystem|zselect|ztcp)\b/, severity: 'critical' },
|
|
162
190
|
{ id: 'BACKSLASH_ESCAPED_OPERATORS', pattern: /\\[|;&]/, severity: 'medium' },
|
|
163
191
|
];
|
|
@@ -176,8 +204,16 @@ function runShellInjectionChecks(command) {
|
|
|
176
204
|
|
|
177
205
|
export { containsSecrets, isDestructiveCommand, runShellInjectionChecks, SECRET_PATTERNS, DESTRUCTIVE_COMMANDS, SHELL_INJECTION_CHECKS };
|
|
178
206
|
|
|
179
|
-
// Only run main when executed directly (not imported by tests)
|
|
207
|
+
// Only run main when executed directly (not imported by tests).
|
|
208
|
+
// realpathSync resolves symlinks on both sides — important on macOS where
|
|
209
|
+
// /tmp is a symlink to /private/tmp. Without realpath the comparison fails
|
|
210
|
+
// silently and main() never runs.
|
|
180
211
|
import { fileURLToPath } from 'url';
|
|
181
|
-
|
|
182
|
-
|
|
212
|
+
import { realpathSync } from 'fs';
|
|
213
|
+
if (process.argv[1]) {
|
|
214
|
+
try {
|
|
215
|
+
if (realpathSync(process.argv[1]) === realpathSync(fileURLToPath(import.meta.url))) {
|
|
216
|
+
main();
|
|
217
|
+
}
|
|
218
|
+
} catch { /* path resolution failed — not invoked as a script */ }
|
|
183
219
|
}
|
|
@@ -3,217 +3,110 @@
|
|
|
3
3
|
* License Guard Hook — UserPromptSubmit
|
|
4
4
|
*
|
|
5
5
|
* Validates license status before each user prompt turn.
|
|
6
|
-
* Uses a
|
|
6
|
+
* Uses a 4-hour cache stored in ~/.chati-dev/license.yaml (global, all projects).
|
|
7
7
|
*
|
|
8
8
|
* Behavior:
|
|
9
|
-
* - No
|
|
10
|
-
* - Cache VALID <
|
|
11
|
-
* - Cache EXPIRED/INVALID
|
|
12
|
-
* - Cache stale (>
|
|
13
|
-
* - API unreachable
|
|
9
|
+
* - No license file → block with activation instructions
|
|
10
|
+
* - Cache VALID < 4h → allow silently
|
|
11
|
+
* - Cache EXPIRED/INVALID → block with renewal message
|
|
12
|
+
* - Cache stale (> 4h) → call API, refresh cache, then allow or block
|
|
13
|
+
* - API unreachable + cache VALID < 24h → grace allow
|
|
14
|
+
* - API unreachable + cache stale → block (cannot verify)
|
|
15
|
+
*
|
|
16
|
+
* Telemetry pings are owned by session-digest.js (PreCompact event), not here.
|
|
14
17
|
*/
|
|
15
18
|
|
|
16
19
|
import { existsSync, readFileSync, writeFileSync, mkdirSync } from 'fs';
|
|
17
|
-
import { join
|
|
20
|
+
import { join } from 'path';
|
|
18
21
|
import { homedir } from 'os';
|
|
19
22
|
|
|
20
23
|
const API_BASE = 'https://chati.dev/api';
|
|
21
|
-
const
|
|
22
|
-
const CACHE_TTL_MS = 4 * 60 * 60 * 1000; // 4 hours — shorter window to catch expirations faster
|
|
23
|
-
const PING_THROTTLE_MS = 5 * 60 * 1000; // 5 minutes
|
|
24
|
+
const CACHE_TTL_MS = 4 * 60 * 60 * 1000; // 4 hours
|
|
24
25
|
|
|
25
26
|
const GLOBAL_DIR = join(homedir(), '.chati-dev');
|
|
26
27
|
const LICENSE_PATH = join(GLOBAL_DIR, 'license.yaml');
|
|
27
|
-
const PINGS_PATH = join(GLOBAL_DIR, 'pings.yaml');
|
|
28
|
-
|
|
29
|
-
async function main() {
|
|
30
|
-
let input = '';
|
|
31
|
-
for await (const chunk of process.stdin) input += chunk;
|
|
32
|
-
|
|
33
|
-
try {
|
|
34
|
-
// Read global license file — REQUIRED for operation
|
|
35
|
-
if (!existsSync(LICENSE_PATH)) {
|
|
36
|
-
block('No license found. Run: npx chati-dev activate --key=YOUR-KEY\nGet a license at https://chati.dev/pricing');
|
|
37
|
-
return;
|
|
38
|
-
}
|
|
39
28
|
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
if (age < CACHE_TTL_MS) {
|
|
54
|
-
if (status === 'VALID') {
|
|
55
|
-
await allowWithPing(licenseKey);
|
|
56
|
-
return;
|
|
57
|
-
}
|
|
58
|
-
if (status === 'EXPIRED' || status === 'INVALID') {
|
|
59
|
-
block(buildMessage(status, readYamlField(licenseRaw, 'reason')));
|
|
60
|
-
return;
|
|
61
|
-
}
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
// Cache stale — call API
|
|
65
|
-
try {
|
|
66
|
-
const machineId = await computeMachineId();
|
|
67
|
-
const res = await fetch(
|
|
68
|
-
`${API_BASE}/license/validate?key=${encodeURIComponent(licenseKey)}&machine_id=${encodeURIComponent(machineId)}`,
|
|
69
|
-
{ signal: AbortSignal.timeout(5000) }
|
|
70
|
-
);
|
|
71
|
-
const data = await res.json();
|
|
29
|
+
/**
|
|
30
|
+
* Pure license validation function — provider-agnostic.
|
|
31
|
+
* Returns a verdict object that hook wrappers can translate to their
|
|
32
|
+
* provider's blocking schema (Claude UserPromptSubmit, Gemini BeforeModel,
|
|
33
|
+
* Codex UserPromptSubmit).
|
|
34
|
+
*
|
|
35
|
+
* @returns {Promise<{valid: boolean, reason?: string, status?: string}>}
|
|
36
|
+
*/
|
|
37
|
+
export async function checkLicense() {
|
|
38
|
+
// Read global license file — REQUIRED for operation
|
|
39
|
+
if (!existsSync(LICENSE_PATH)) {
|
|
40
|
+
return { valid: false, reason: 'No license found. Run: npx chati-dev activate --key=YOUR-KEY\nGet a license at https://chati.dev/pricing', status: 'MISSING' };
|
|
41
|
+
}
|
|
72
42
|
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
const updated = {
|
|
76
|
-
...existing,
|
|
77
|
-
status: data.status,
|
|
78
|
-
plan: data.plan ?? '',
|
|
79
|
-
days_remaining: data.days_remaining ?? '',
|
|
80
|
-
expires_at: data.expires_at ?? '',
|
|
81
|
-
reason: data.reason ?? '',
|
|
82
|
-
checked_at: new Date().toISOString(),
|
|
83
|
-
};
|
|
84
|
-
mkdirSync(GLOBAL_DIR, { recursive: true });
|
|
85
|
-
writeFileSync(LICENSE_PATH, dumpYaml(updated));
|
|
43
|
+
const licenseRaw = readFileSync(LICENSE_PATH, 'utf-8');
|
|
44
|
+
const licenseKey = readYamlField(licenseRaw, 'key');
|
|
86
45
|
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
return;
|
|
90
|
-
}
|
|
91
|
-
block(buildMessage(data.status, data.reason));
|
|
92
|
-
} catch {
|
|
93
|
-
// API unreachable — check if we have a recent VALID cache to fall back on
|
|
94
|
-
// If cache is less than 24h old AND was VALID, allow (grace period for network issues)
|
|
95
|
-
// Otherwise block — we cannot verify the license
|
|
96
|
-
if (age < 24 * 60 * 60 * 1000 && status === 'VALID') {
|
|
97
|
-
allow(); // grace: last check was recent and valid, allow despite network issue
|
|
98
|
-
} else {
|
|
99
|
-
block('Unable to verify license (network issue). If this persists, check your connection.\nRun: npx chati-dev activate --key=YOUR-KEY');
|
|
100
|
-
}
|
|
101
|
-
}
|
|
102
|
-
} catch { /* expected: stdin parse may fail — fail open */
|
|
103
|
-
allow();
|
|
46
|
+
if (!licenseKey || licenseKey === 'null') {
|
|
47
|
+
return { valid: false, reason: 'No license key configured. Run: npx chati-dev activate --key=YOUR-KEY\nGet a license at https://chati.dev/pricing', status: 'MISSING' };
|
|
104
48
|
}
|
|
105
|
-
}
|
|
106
49
|
|
|
107
|
-
//
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
async function allowWithPing(licenseKey) {
|
|
112
|
-
const projectPath = process.cwd();
|
|
113
|
-
const projectName = basename(projectPath) || 'unknown';
|
|
114
|
-
const session = readSessionContext();
|
|
50
|
+
// Check cache validity
|
|
51
|
+
const status = readYamlField(licenseRaw, 'status');
|
|
52
|
+
const checkedAt = readYamlField(licenseRaw, 'checked_at');
|
|
53
|
+
const age = checkedAt ? Date.now() - new Date(checkedAt).getTime() : Infinity;
|
|
115
54
|
|
|
116
|
-
if (
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
} else {
|
|
122
|
-
allow();
|
|
55
|
+
if (age < CACHE_TTL_MS) {
|
|
56
|
+
if (status === 'VALID') return { valid: true, status: 'VALID' };
|
|
57
|
+
if (status === 'EXPIRED' || status === 'INVALID') {
|
|
58
|
+
return { valid: false, reason: buildMessage(status, readYamlField(licenseRaw, 'reason')), status };
|
|
59
|
+
}
|
|
123
60
|
}
|
|
124
|
-
}
|
|
125
61
|
|
|
126
|
-
|
|
127
|
-
* Reads .chati/session.yaml from cwd to extract pipeline phase, agent, and project type.
|
|
128
|
-
* Returns {} on any error (fail silently — this is optional enrichment).
|
|
129
|
-
*/
|
|
130
|
-
function readSessionContext() {
|
|
62
|
+
// Cache stale — call API
|
|
131
63
|
try {
|
|
132
|
-
const
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
const
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
const
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
64
|
+
const machineId = await computeMachineId();
|
|
65
|
+
const res = await fetch(
|
|
66
|
+
`${API_BASE}/license/validate?key=${encodeURIComponent(licenseKey)}&machine_id=${encodeURIComponent(machineId)}`,
|
|
67
|
+
{ signal: AbortSignal.timeout(5000) }
|
|
68
|
+
);
|
|
69
|
+
const data = await res.json();
|
|
70
|
+
|
|
71
|
+
// Persist updated status (keep existing key)
|
|
72
|
+
const existing = parseYaml(licenseRaw);
|
|
73
|
+
const updated = {
|
|
74
|
+
...existing,
|
|
75
|
+
status: data.status,
|
|
76
|
+
plan: data.plan ?? '',
|
|
77
|
+
days_remaining: data.days_remaining ?? '',
|
|
78
|
+
expires_at: data.expires_at ?? '',
|
|
79
|
+
reason: data.reason ?? '',
|
|
80
|
+
checked_at: new Date().toISOString(),
|
|
81
|
+
};
|
|
82
|
+
mkdirSync(GLOBAL_DIR, { recursive: true });
|
|
83
|
+
writeFileSync(LICENSE_PATH, dumpYaml(updated));
|
|
84
|
+
|
|
85
|
+
if (data.status === 'VALID') return { valid: true, status: 'VALID' };
|
|
86
|
+
return { valid: false, reason: buildMessage(data.status, data.reason), status: data.status };
|
|
87
|
+
} catch {
|
|
88
|
+
// API unreachable — grace period if cache is recent and was VALID
|
|
89
|
+
if (age < 24 * 60 * 60 * 1000 && status === 'VALID') {
|
|
90
|
+
return { valid: true, status: 'VALID', graced: true };
|
|
91
|
+
}
|
|
92
|
+
return { valid: false, reason: 'Unable to verify license (network issue). If this persists, check your connection.\nRun: npx chati-dev activate --key=YOUR-KEY', status: 'NETWORK_ERROR' };
|
|
93
|
+
}
|
|
150
94
|
}
|
|
151
95
|
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
*/
|
|
156
|
-
function shouldPing(projectPath, session = {}) {
|
|
157
|
-
try {
|
|
158
|
-
if (!existsSync(PINGS_PATH)) return true;
|
|
159
|
-
const content = readFileSync(PINGS_PATH, 'utf-8');
|
|
160
|
-
const escaped = projectPath.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
161
|
-
const match = content.match(new RegExp(`^${escaped}:\\s*'?([^'\\n]+)'?$`, 'm'));
|
|
162
|
-
if (!match) return true;
|
|
163
|
-
|
|
164
|
-
const [timestamp, lastPhase, lastAgent] = match[1].trim().split('|');
|
|
165
|
-
const age = Date.now() - new Date(timestamp).getTime();
|
|
166
|
-
if (age > PING_THROTTLE_MS) return true;
|
|
167
|
-
|
|
168
|
-
// Phase or agent changed → ping immediately
|
|
169
|
-
if (session.pipeline_phase && session.pipeline_phase !== lastPhase) return true;
|
|
170
|
-
if (session.current_agent && session.current_agent !== lastAgent) return true;
|
|
171
|
-
|
|
172
|
-
return false;
|
|
173
|
-
} catch { return false; } // fail closed — don't add latency on parse error
|
|
174
|
-
}
|
|
96
|
+
async function main() {
|
|
97
|
+
let input = '';
|
|
98
|
+
for await (const chunk of process.stdin) input += chunk;
|
|
175
99
|
|
|
176
|
-
function recordPing(projectPath, session = {}) {
|
|
177
100
|
try {
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
const agent = session.current_agent ?? '';
|
|
182
|
-
const value = `${timestamp}|${phase}|${agent}`;
|
|
183
|
-
|
|
184
|
-
let content = existsSync(PINGS_PATH) ? readFileSync(PINGS_PATH, 'utf-8') : '';
|
|
185
|
-
const escaped = projectPath.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
186
|
-
if (new RegExp(`^${escaped}:`, 'm').test(content)) {
|
|
187
|
-
content = content.replace(new RegExp(`^(${escaped}:).*$`, 'm'), `$1 '${value}'`);
|
|
101
|
+
const result = await checkLicense();
|
|
102
|
+
if (result.valid) {
|
|
103
|
+
allow();
|
|
188
104
|
} else {
|
|
189
|
-
|
|
105
|
+
block(result.reason);
|
|
190
106
|
}
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
}
|
|
194
|
-
|
|
195
|
-
async function sendSessionPing(licenseKey, projectName, session = {}) {
|
|
196
|
-
try {
|
|
197
|
-
const properties = {};
|
|
198
|
-
if (session.pipeline_phase) properties.pipeline_phase = session.pipeline_phase;
|
|
199
|
-
if (session.current_agent) properties.current_agent = session.current_agent;
|
|
200
|
-
if (session.project_type) properties.project_type = session.project_type;
|
|
201
|
-
|
|
202
|
-
await fetch(TELEMETRY_ENDPOINT, {
|
|
203
|
-
method: 'POST',
|
|
204
|
-
headers: { 'Content-Type': 'application/json' },
|
|
205
|
-
body: JSON.stringify({
|
|
206
|
-
license_key: licenseKey,
|
|
207
|
-
project_name: projectName,
|
|
208
|
-
events: [{
|
|
209
|
-
type: 'session_active',
|
|
210
|
-
timestamp: new Date().toISOString(),
|
|
211
|
-
properties,
|
|
212
|
-
}],
|
|
213
|
-
}),
|
|
214
|
-
signal: AbortSignal.timeout(3000),
|
|
215
|
-
});
|
|
216
|
-
} catch {} // fire-and-forget, always fail silently
|
|
107
|
+
} catch { /* expected: stdin parse may fail — fail open */
|
|
108
|
+
allow();
|
|
109
|
+
}
|
|
217
110
|
}
|
|
218
111
|
|
|
219
112
|
// ---------------------------------------------------------------------------
|
|
@@ -221,10 +114,12 @@ async function sendSessionPing(licenseKey, projectName, session = {}) {
|
|
|
221
114
|
// ---------------------------------------------------------------------------
|
|
222
115
|
|
|
223
116
|
function allow() {
|
|
224
|
-
|
|
117
|
+
// UserPromptSubmit canonical "allow" is empty object — omit decision.
|
|
118
|
+
process.stdout.write('{}');
|
|
225
119
|
}
|
|
226
120
|
|
|
227
121
|
function block(reason) {
|
|
122
|
+
// UserPromptSubmit blocking schema: decision + reason at top level.
|
|
228
123
|
process.stdout.write(JSON.stringify({ decision: 'block', reason }));
|
|
229
124
|
}
|
|
230
125
|
|
|
@@ -274,7 +169,16 @@ async function computeMachineId() {
|
|
|
274
169
|
return createHash('sha256').update(raw).digest('hex').substring(0, 16);
|
|
275
170
|
}
|
|
276
171
|
|
|
172
|
+
// Only run main when executed directly (not imported by tests).
|
|
173
|
+
// realpathSync resolves symlinks on both sides — important on macOS where
|
|
174
|
+
// /tmp is a symlink to /private/tmp. Without realpath the comparison fails
|
|
175
|
+
// silently and main() never runs.
|
|
277
176
|
import { fileURLToPath } from 'url';
|
|
278
|
-
|
|
279
|
-
|
|
177
|
+
import { realpathSync } from 'fs';
|
|
178
|
+
if (process.argv[1]) {
|
|
179
|
+
try {
|
|
180
|
+
if (realpathSync(process.argv[1]) === realpathSync(fileURLToPath(import.meta.url))) {
|
|
181
|
+
main();
|
|
182
|
+
}
|
|
183
|
+
} catch { /* path resolution failed — not invoked as a script */ }
|
|
280
184
|
}
|