chati-dev 4.4.1 → 4.5.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/README.md +29 -27
- package/bin/chati.js +235 -1
- package/framework/agents/plan/tasks.md +46 -1
- package/framework/config.yaml +2 -2
- package/framework/constitution.md +13 -16
- package/framework/context/governance.md +5 -5
- package/framework/context/root.md +5 -5
- package/framework/i18n/en.yaml +8 -2
- package/framework/i18n/es.yaml +8 -2
- package/framework/i18n/fr.yaml +8 -2
- package/framework/i18n/pt.yaml +8 -2
- package/framework/manifest.json +21 -21
- package/framework/manifest.sig +1 -1
- package/framework/orchestrator/chati.md +43 -12
- package/node_modules/@chati/browser-capability/README.md +10 -0
- package/node_modules/@chati/browser-capability/package.json +17 -0
- package/node_modules/@chati/browser-capability/src/index.js +165 -0
- package/node_modules/@chati/core/package.json +13 -0
- package/node_modules/@chati/core/src/index.js +111 -0
- package/node_modules/@chati/knowledge-context/package.json +17 -0
- package/node_modules/@chati/knowledge-context/src/index.js +202 -0
- package/node_modules/@chati/planning/package.json +17 -0
- package/node_modules/@chati/planning/src/index.js +367 -0
- package/node_modules/@chati/provider-registry/package.json +16 -0
- package/node_modules/@chati/provider-registry/src/index.js +148 -0
- package/node_modules/@chati/rail/README.md +24 -0
- package/node_modules/@chati/rail/package.json +19 -0
- package/node_modules/@chati/rail/src/index.js +437 -0
- package/node_modules/@chati/release-lane/README.md +24 -0
- package/node_modules/@chati/release-lane/package.json +17 -0
- package/node_modules/@chati/release-lane/src/index.js +172 -0
- package/node_modules/@chati/review-council/package.json +17 -0
- package/node_modules/@chati/review-council/src/index.js +264 -0
- package/node_modules/@chati/tracking-clickup/README.md +55 -0
- package/node_modules/@chati/tracking-clickup/package.json +17 -0
- package/node_modules/@chati/tracking-clickup/src/index.js +293 -0
- package/package.json +25 -3
- package/src/config/ide-configs.js +11 -0
- package/src/context/domain-loader.js +1 -1
- package/src/dashboard/data-reader.js +1 -1
- package/src/executors/runner.js +1 -1
- package/src/installer/core.js +14 -13
- package/src/installer/provider-overlay.js +8 -15
- package/src/installer/scaffold-applier.js +1 -1
- package/src/installer/templates.js +12 -25
- package/src/installer/validator.js +5 -10
- package/src/installer-v2/catalog-client.js +165 -0
- package/src/installer-v2/index.js +304 -0
- package/src/installer-v2/model-catalog-envelope.json +278 -0
- package/src/installer-v2/model-catalog.json +130 -0
- package/src/installer-v2/model-catalog.sig +1 -0
- package/src/installer-v2/wizard-installation.js +116 -0
- package/src/intelligence/registry-manager.js +1 -1
- package/src/license/client.js +27 -1
- package/src/memory/session-digest.js +1 -1
- package/src/merger/yaml-merger.js +1 -1
- package/src/orchestrator/browser-runtime.js +25 -0
- package/src/orchestrator/cli.js +93 -8
- package/src/orchestrator/clickup-projection.js +84 -0
- package/src/orchestrator/clickup-runtime.js +13 -0
- package/src/orchestrator/knowledge-runtime.js +64 -0
- package/src/orchestrator/planning-runtime.js +127 -0
- package/src/orchestrator/rail-runtime.js +421 -0
- package/src/orchestrator/release-runtime.js +14 -0
- package/src/orchestrator/review-runtime.js +74 -0
- package/src/orchestrator/runtime-installation-v2.js +57 -0
- package/src/orchestrator/session-manager.js +1 -1
- package/src/telemetry/config.js +1 -1
- package/src/terminal/adapters/grok-adapter.js +16 -0
- package/src/terminal/adapters/index.js +1 -0
- package/src/terminal/cli-registry.js +20 -5
- package/src/terminal/prompt-builder.js +4 -2
- package/src/terminal/run-agent.js +3 -0
- package/src/terminal/run-parallel.js +21 -10
- package/src/terminal/spawner.js +7 -1
- package/src/terminal/team-task-list.js +1 -1
- package/src/upgrade/checker.js +1 -1
- package/src/upgrade/migrator.js +1 -1
- package/src/utils/config-parser.js +3 -8
- package/src/wizard/i18n.js +10 -4
- package/src/wizard/index.js +49 -17
- package/src/wizard/questions.js +50 -28
package/framework/i18n/fr.yaml
CHANGED
|
@@ -22,8 +22,14 @@ installer:
|
|
|
22
22
|
llm_provider_not_installed: "(CLI non detecte)"
|
|
23
23
|
llm_provider_label: "Fournisseur IA"
|
|
24
24
|
ide_selection_title: "Selectionnez vos IDEs / CLIs (multiples autorises):"
|
|
25
|
-
|
|
26
|
-
|
|
25
|
+
installation_mode_title: "Selectionnez le mode d'installation"
|
|
26
|
+
installation_mode_open: "Open"
|
|
27
|
+
installation_mode_open_hint: "Framework public sans integrations d'entreprise obligatoires"
|
|
28
|
+
installation_mode_internal: "Internal"
|
|
29
|
+
installation_mode_internal_hint: "Necessite une licence autorisee et active le suivi ClickUp et les politiques internes"
|
|
30
|
+
installation_mode_label: "Mode d'installation"
|
|
31
|
+
model_routing_label: "Routage des modeles"
|
|
32
|
+
model_routing_automatic: "Automatique par tache entre tous les modeles actives"
|
|
27
33
|
providers_label: "Fournisseurs"
|
|
28
34
|
quick_start_switch_hint: "Changez de CLI a tout moment — votre session continue la ou vous l'avez laissee"
|
|
29
35
|
created_overlays: "Crees les repertoires d'overlay des fournisseurs"
|
package/framework/i18n/pt.yaml
CHANGED
|
@@ -22,8 +22,14 @@ installer:
|
|
|
22
22
|
llm_provider_not_installed: "(CLI nao detectada)"
|
|
23
23
|
llm_provider_label: "Provedor de IA"
|
|
24
24
|
ide_selection_title: "Selecione suas IDEs / CLIs (multiplos permitidos):"
|
|
25
|
-
|
|
26
|
-
|
|
25
|
+
installation_mode_title: "Selecione o modo de instalacao"
|
|
26
|
+
installation_mode_open: "Open"
|
|
27
|
+
installation_mode_open_hint: "Framework publico sem integracoes corporativas obrigatorias"
|
|
28
|
+
installation_mode_internal: "Internal"
|
|
29
|
+
installation_mode_internal_hint: "Exige uma licenca autorizada e ativa o tracking no ClickUp e as politicas internas"
|
|
30
|
+
installation_mode_label: "Modo de instalacao"
|
|
31
|
+
model_routing_label: "Roteamento de modelos"
|
|
32
|
+
model_routing_automatic: "Automatico por task entre todos os modelos habilitados"
|
|
27
33
|
providers_label: "Provedores"
|
|
28
34
|
quick_start_switch_hint: "Troque de CLI a qualquer momento — sua sessao continua de onde parou"
|
|
29
35
|
created_overlays: "Criados diretorios de overlay de provedores"
|
package/framework/manifest.json
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
{
|
|
2
|
-
"createdAt": "2026-
|
|
2
|
+
"createdAt": "2026-08-27T07:27:47.925Z",
|
|
3
3
|
"files": {
|
|
4
4
|
"agents/build/dev.md": {
|
|
5
5
|
"hash": "5c8da2eee3357bbe84a6c468c76fe60a0c77357575503c3797aa94128282c565",
|
|
@@ -52,8 +52,8 @@
|
|
|
52
52
|
"type": "md"
|
|
53
53
|
},
|
|
54
54
|
"agents/plan/tasks.md": {
|
|
55
|
-
"hash": "
|
|
56
|
-
"size":
|
|
55
|
+
"hash": "e43665228fd7ae5a239e2d4b4298a54c5f06f465a89c9b8e939cf6e6b2ac8089",
|
|
56
|
+
"size": 28251,
|
|
57
57
|
"type": "md"
|
|
58
58
|
},
|
|
59
59
|
"agents/plan/ux-brand-architect.md": {
|
|
@@ -97,18 +97,18 @@
|
|
|
97
97
|
"type": "md"
|
|
98
98
|
},
|
|
99
99
|
"config.yaml": {
|
|
100
|
-
"hash": "
|
|
100
|
+
"hash": "f7c77483aeaa44f37d8492d85a376304cb24ec28d91dbed3155f41faa627b7ad",
|
|
101
101
|
"size": 2841,
|
|
102
102
|
"type": "yaml"
|
|
103
103
|
},
|
|
104
104
|
"constitution.md": {
|
|
105
|
-
"hash": "
|
|
106
|
-
"size":
|
|
105
|
+
"hash": "444fa267ed5f998c428611c61204e390b694ed744913834d06faed9fce543e55",
|
|
106
|
+
"size": 67858,
|
|
107
107
|
"type": "md"
|
|
108
108
|
},
|
|
109
109
|
"context/governance.md": {
|
|
110
|
-
"hash": "
|
|
111
|
-
"size":
|
|
110
|
+
"hash": "80b2243a84b3ec0e9066ee51251685605252e4bd47fa94c2ce25f00af688cd79",
|
|
111
|
+
"size": 6086,
|
|
112
112
|
"type": "md"
|
|
113
113
|
},
|
|
114
114
|
"context/protocols.md": {
|
|
@@ -122,8 +122,8 @@
|
|
|
122
122
|
"type": "md"
|
|
123
123
|
},
|
|
124
124
|
"context/root.md": {
|
|
125
|
-
"hash": "
|
|
126
|
-
"size":
|
|
125
|
+
"hash": "1ac04cbce1e85009ecdf224cdbee4fe64bcc80232366c2122805e9c9c3191f9a",
|
|
126
|
+
"size": 1217,
|
|
127
127
|
"type": "md"
|
|
128
128
|
},
|
|
129
129
|
"data/entity-registry.yaml": {
|
|
@@ -377,23 +377,23 @@
|
|
|
377
377
|
"type": "js"
|
|
378
378
|
},
|
|
379
379
|
"i18n/en.yaml": {
|
|
380
|
-
"hash": "
|
|
381
|
-
"size":
|
|
380
|
+
"hash": "5eb0e9dfa7baec166fc12ca8a1c0e9623c9a023c51c09455b80a6d8df1e0205f",
|
|
381
|
+
"size": 4941,
|
|
382
382
|
"type": "yaml"
|
|
383
383
|
},
|
|
384
384
|
"i18n/es.yaml": {
|
|
385
|
-
"hash": "
|
|
386
|
-
"size":
|
|
385
|
+
"hash": "1ed8c732c13c23e3fc2d5578526c1285a2a1ffc1d79b85d8e12365c3bcbbd882",
|
|
386
|
+
"size": 5225,
|
|
387
387
|
"type": "yaml"
|
|
388
388
|
},
|
|
389
389
|
"i18n/fr.yaml": {
|
|
390
|
-
"hash": "
|
|
391
|
-
"size":
|
|
390
|
+
"hash": "ae88b914ea39d6613879aae3a3fcdf92a771d3df0f207def6cbb2cc76455cfe9",
|
|
391
|
+
"size": 5290,
|
|
392
392
|
"type": "yaml"
|
|
393
393
|
},
|
|
394
394
|
"i18n/pt.yaml": {
|
|
395
|
-
"hash": "
|
|
396
|
-
"size":
|
|
395
|
+
"hash": "9e766ca8c7eba6362816bdc9d1f6162d615f1b5ca1a9b2f75071a53b7ab9f6b3",
|
|
396
|
+
"size": 5163,
|
|
397
397
|
"type": "yaml"
|
|
398
398
|
},
|
|
399
399
|
"intelligence/confidence.yaml": {
|
|
@@ -472,8 +472,8 @@
|
|
|
472
472
|
"type": "md"
|
|
473
473
|
},
|
|
474
474
|
"orchestrator/chati.md": {
|
|
475
|
-
"hash": "
|
|
476
|
-
"size":
|
|
475
|
+
"hash": "c2d895fd342422985b68d4912189f2abcfb2bac536c5cf325d0480faf9db5f9f",
|
|
476
|
+
"size": 44348,
|
|
477
477
|
"type": "md"
|
|
478
478
|
},
|
|
479
479
|
"patterns/elicitation-library.yaml": {
|
|
@@ -1347,5 +1347,5 @@
|
|
|
1347
1347
|
"type": "yaml"
|
|
1348
1348
|
}
|
|
1349
1349
|
},
|
|
1350
|
-
"version": "4.
|
|
1350
|
+
"version": "4.5.2"
|
|
1351
1351
|
}
|
package/framework/manifest.sig
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
|
|
1
|
+
Y5XGGQ07UbiwrVZytFSTJvXNifA8l16S9QI+ret6AVjrYfWvN6E5ChUG0BGKWLsE2wpjVtEW4TVJosYaFDANAg==
|
|
@@ -100,6 +100,7 @@ Parse the JSON output. The `action` field tells you what to do:
|
|
|
100
100
|
| `spawn_autonomous` | Action: Autonomous Agent |
|
|
101
101
|
| `spawn_parallel` | Action: Parallel Spawn |
|
|
102
102
|
| `spawn_team` | Action: Spawn Team (Article XXI) |
|
|
103
|
+
| `rail_execute` | Action: RAIL Execution |
|
|
103
104
|
| `resume` | Action: Resume |
|
|
104
105
|
| `user_preview` | Action: User Preview |
|
|
105
106
|
| `complete` | Action: Complete |
|
|
@@ -118,8 +119,7 @@ Only mention the bracket when it reaches CRITICAL, and only in plain language. E
|
|
|
118
119
|
- `ux` → "let's design the user experience"
|
|
119
120
|
- `phases` / `tasks` → "let's break this down into phases"
|
|
120
121
|
- `qa-planning` / `qa-implementation` → "let's review what we have"
|
|
121
|
-
- `
|
|
122
|
-
- `devops` → "let's deploy it"
|
|
122
|
+
- `rail` → "let's execute the approved plan"
|
|
123
123
|
|
|
124
124
|
---
|
|
125
125
|
|
|
@@ -314,8 +314,8 @@ Planning phase agents (detail, architect, ux) run simultaneously.
|
|
|
314
314
|
|
|
315
315
|
### Parallelization Rules
|
|
316
316
|
- GROUP 1 (Planning Team): detail, architect, ux, qa-planning - run as team (QA-Planning activates after trio completes)
|
|
317
|
-
-
|
|
318
|
-
- NOT parallelizable: WU, Brief, Phases, Tasks
|
|
317
|
+
- RAIL execution: scheduling, reviews and bounded rework are owned by the sealed project-local RAIL handoff.
|
|
318
|
+
- NOT parallelizable: WU, Brief, Phases, Tasks. After Tasks is sealed, do not create a legacy Build Team.
|
|
319
319
|
|
|
320
320
|
---
|
|
321
321
|
|
|
@@ -397,7 +397,11 @@ Agent tool call 4:
|
|
|
397
397
|
Write your handoff to artifacts/handoffs/qa-planning-handoff.md when done.]
|
|
398
398
|
```
|
|
399
399
|
|
|
400
|
-
**For Build Team** (BUILD phase) - Wave-Based Pipeline:
|
|
400
|
+
**Legacy compatibility only, installations without `.chati/v2/installation.json`: For Build Team** (BUILD phase) - Wave-Based Pipeline:
|
|
401
|
+
|
|
402
|
+
For a v2 installation this section MUST NOT run. The `tasks` handoff returns
|
|
403
|
+
`rail_execute`; use **Action: RAIL Execution** instead. Legacy Build and DevOps
|
|
404
|
+
remain documented only to let older installed projects finish safely.
|
|
401
405
|
|
|
402
406
|
The Build Team operates in WAVES, not continuous parallel polling.
|
|
403
407
|
Each wave spawns Dev + QA with bounded, independent work.
|
|
@@ -610,8 +614,8 @@ Team member {agent} appears stuck in a repetition loop (Article XXII Echo Detect
|
|
|
610
614
|
After Planning Team (detail + architect + ux + qa-planning) completes and user approves:
|
|
611
615
|
1. Dissolve Planning Team: `team-dissolve --team-id {planning-team-id}`
|
|
612
616
|
2. Continue pipeline normally through Phases, Tasks (these are NOT parallelizable and run sequentially as interactive or autonomous agents). QA-Planning already ran inside the Planning Team.
|
|
613
|
-
3.
|
|
614
|
-
4.
|
|
617
|
+
3. After Tasks seals a v2 handoff, follow **Action: RAIL Execution**. Do not form a Build Team.
|
|
618
|
+
4. Only a pre-v2 installation may activate the legacy Build Team wave mode.
|
|
615
619
|
|
|
616
620
|
**Team cleanup order:** Dissolve current team BEFORE creating next team. Never two teams active simultaneously.
|
|
617
621
|
|
|
@@ -619,7 +623,7 @@ After Planning Team (detail + architect + ux + qa-planning) completes and user a
|
|
|
619
623
|
|
|
620
624
|
If the Agent tool is unavailable or any sub-agent spawn fails:
|
|
621
625
|
```
|
|
622
|
-
1. Fall back silently to spawn_parallel
|
|
626
|
+
1. Fall back silently to spawn_parallel for Planning. RAIL never falls back to the legacy Build pipeline.
|
|
623
627
|
Use the existing Action: Parallel Spawn or Action: Autonomous Agent flow
|
|
624
628
|
2. Do NOT display any error or fallback message to the user
|
|
625
629
|
3. Run: node .chati.dev/orchestrator/chati-router.js team-dissolve --team-id {team_id}
|
|
@@ -631,6 +635,32 @@ If the Agent tool is unavailable or any sub-agent spawn fails:
|
|
|
631
635
|
|
|
632
636
|
---
|
|
633
637
|
|
|
638
|
+
## Action: RAIL Execution
|
|
639
|
+
|
|
640
|
+
The planning handoff is sealed. RAIL is now the sole authority for execution,
|
|
641
|
+
review, bounded rework, ClickUp projection and the Release Lane.
|
|
642
|
+
|
|
643
|
+
1. Read the immutable handoff id returned in `rail_execute`.
|
|
644
|
+
2. Run:
|
|
645
|
+
```
|
|
646
|
+
node .chati.dev/_cli/bin/chati.js rail-next --handoff-id {handoff_id}
|
|
647
|
+
```
|
|
648
|
+
3. Execute only a dependency-ready task. Use its selected provider and model
|
|
649
|
+
binding from `.chati/v2/installation.json`. Never substitute an unselected
|
|
650
|
+
vendor or model.
|
|
651
|
+
4. Record every claim, review, completion, rework count and ClickUp projection
|
|
652
|
+
through RAIL. ClickUp is a projection, while the local journal remains
|
|
653
|
+
canonical during an outage. For the internal profile, a merged completion
|
|
654
|
+
MUST include `actual_human_effort`, `actual_ai_processing`, `rework_cycles`
|
|
655
|
+
and one verified review reference per rework cycle. RAIL writes the
|
|
656
|
+
in-progress, ready-to-merge and closed projections automatically; do not
|
|
657
|
+
create a parallel manual tracking path.
|
|
658
|
+
5. After two normal rework cycles, use the configured top-tier adjudication
|
|
659
|
+
route. Escalate to a human only for an authority decision, not an ordinary
|
|
660
|
+
technical correction.
|
|
661
|
+
6. When `rail-next` has no ready work, check the Release Lane. It may prepare
|
|
662
|
+
evidence, but cannot deploy or publish without explicit authorization.
|
|
663
|
+
|
|
634
664
|
## Action: Resume
|
|
635
665
|
|
|
636
666
|
The user is returning to an active session.
|
|
@@ -682,10 +712,11 @@ DISCOVER:
|
|
|
682
712
|
PLAN:
|
|
683
713
|
Detail: {score}% Arch: {score}% UX: {score}%
|
|
684
714
|
Phases: {score}% Tasks: {score}% QA-P: {score}%
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
715
|
+
RAIL:
|
|
716
|
+
Handoff: {handoff_id} Ready tasks: {count}
|
|
717
|
+
Rework: {count} ClickUp projection: {status}
|
|
718
|
+
RELEASE LANE:
|
|
719
|
+
Evidence: {status} Authorization: {status}
|
|
689
720
|
|
|
690
721
|
Current Agent: {current_agent}
|
|
691
722
|
Backlog: {count} items ({high_priority} high priority)
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
# CHATI Browser Capability
|
|
2
|
+
|
|
3
|
+
G3 implementation of C-10. This package is policy-only and constructs its own
|
|
4
|
+
non-injectable fake adapter. It has no browser launch, MCP, session,
|
|
5
|
+
credential, network or scheduling code. A task that declares `requires_browser: true` must
|
|
6
|
+
pass `assertBrowserCapabilityForTask` before it can reach the fake adapter.
|
|
7
|
+
|
|
8
|
+
The returned `BrowserEvidenceManifest` contains the task policy, classification,
|
|
9
|
+
redaction and retention constraints. Artifacts are references only and must be
|
|
10
|
+
redacted before recording.
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@chati/browser-capability",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"private": true,
|
|
5
|
+
"description": "Policy-only browser capability contract for CHATI",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"exports": "./src/index.js",
|
|
8
|
+
"dependencies": {
|
|
9
|
+
"@chati/core": "0.1.0"
|
|
10
|
+
},
|
|
11
|
+
"scripts": {
|
|
12
|
+
"test": "node --test test/**/*.test.js"
|
|
13
|
+
},
|
|
14
|
+
"engines": {
|
|
15
|
+
"node": ">=20.0.0"
|
|
16
|
+
}
|
|
17
|
+
}
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
import { ContractError, canonicalize, sha256 } from '@chati/core';
|
|
2
|
+
|
|
3
|
+
const MODES = new Set(['unauthenticated', 'authenticated']);
|
|
4
|
+
const TOOLS = new Set(['playwright-cli', 'playwright-mcp', 'chrome-devtools-mcp', 'approved-other']);
|
|
5
|
+
const CLASSIFICATIONS = new Set(['internal', 'confidential', 'client-restricted', 'personal-restricted']);
|
|
6
|
+
const ACTION_CONFIRMATIONS = new Set(['required', 'not-required-by-policy']);
|
|
7
|
+
const ARTIFACT_KINDS = new Set(['screenshot', 'dom', 'network', 'trace', 'download', 'console']);
|
|
8
|
+
|
|
9
|
+
function fail(code, message, details = {}) { throw new ContractError(code, message, details); }
|
|
10
|
+
function object(value, code, label) {
|
|
11
|
+
if (!value || typeof value !== 'object' || Array.isArray(value)) fail(code, `${label} must be an object`);
|
|
12
|
+
}
|
|
13
|
+
function string(value, code, label) {
|
|
14
|
+
if (typeof value !== 'string' || value.trim() === '') fail(code, `${label} must be a non-empty string`);
|
|
15
|
+
}
|
|
16
|
+
function ref(value, code, label) { string(value, code, label); }
|
|
17
|
+
function freeze(value) {
|
|
18
|
+
if (value && typeof value === 'object' && !Object.isFrozen(value)) {
|
|
19
|
+
Object.freeze(value);
|
|
20
|
+
for (const child of Object.values(value)) freeze(child);
|
|
21
|
+
}
|
|
22
|
+
return value;
|
|
23
|
+
}
|
|
24
|
+
function normalizedDomain(value) {
|
|
25
|
+
string(value, 'INVALID_ALLOWED_DOMAIN', 'allowed domain');
|
|
26
|
+
const domain = value.trim().toLowerCase();
|
|
27
|
+
if (domain.includes('://') || domain.includes('/') || domain.includes('@') || domain.includes(':') || domain === 'localhost' || /^\d{1,3}(?:\.\d{1,3}){3}$/.test(domain)) {
|
|
28
|
+
fail('INVALID_ALLOWED_DOMAIN', 'allowed_domains must contain a DNS domain, not a URL, host profile, localhost, or IP address');
|
|
29
|
+
}
|
|
30
|
+
if (!/^(?:[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?\.)+[a-z]{2,63}$/.test(domain)) fail('INVALID_ALLOWED_DOMAIN', 'allowed domain is not a valid DNS name');
|
|
31
|
+
return domain;
|
|
32
|
+
}
|
|
33
|
+
function urlDomain(url) {
|
|
34
|
+
string(url, 'INVALID_BROWSER_URL', 'url');
|
|
35
|
+
let parsed;
|
|
36
|
+
try { parsed = new URL(url); } catch { fail('INVALID_BROWSER_URL', 'url must be absolute and parseable'); }
|
|
37
|
+
if (parsed.protocol !== 'https:' && parsed.protocol !== 'http:') fail('INVALID_BROWSER_URL', 'only http and https URLs are supported by the fake preflight');
|
|
38
|
+
if (parsed.username || parsed.password) fail('URL_CREDENTIALS_FORBIDDEN', 'URLs with embedded credentials are forbidden');
|
|
39
|
+
return parsed.hostname.toLowerCase();
|
|
40
|
+
}
|
|
41
|
+
function domainAllowed(hostname, allowedDomains) {
|
|
42
|
+
return allowedDomains.some((domain) => hostname === domain || hostname.endsWith(`.${domain}`));
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/** C-10 policy. A policy describes permission, it never launches a browser. */
|
|
46
|
+
export function validateBrowserPolicy(policy) {
|
|
47
|
+
object(policy, 'INVALID_BROWSER_POLICY', 'browser policy');
|
|
48
|
+
for (const field of ['policy_id', 'task_id', 'mode', 'tool', 'profile_isolation', 'action_confirmation']) string(policy[field], 'INVALID_BROWSER_POLICY', `browser policy.${field}`);
|
|
49
|
+
if (!MODES.has(policy.mode)) fail('INVALID_BROWSER_MODE', 'mode must be unauthenticated or authenticated');
|
|
50
|
+
if (!TOOLS.has(policy.tool)) fail('INVALID_BROWSER_TOOL', 'tool is not an approved browser tool identifier');
|
|
51
|
+
if (policy.profile_isolation !== 'required') fail('PROFILE_ISOLATION_REQUIRED', 'browser policy requires an isolated profile');
|
|
52
|
+
if (!Array.isArray(policy.allowed_domains) || policy.allowed_domains.length === 0) fail('MISSING_ALLOWED_DOMAINS', 'allowed_domains must be a non-empty array');
|
|
53
|
+
const allowed_domains = policy.allowed_domains.map(normalizedDomain);
|
|
54
|
+
if (new Set(allowed_domains).size !== allowed_domains.length) fail('DUPLICATE_ALLOWED_DOMAIN', 'allowed_domains must be unique');
|
|
55
|
+
object(policy.artifact_policy, 'MISSING_ARTIFACT_POLICY', 'artifact_policy');
|
|
56
|
+
if (policy.artifact_policy.redaction !== 'required') fail('REDACTION_REQUIRED', 'artifact_policy.redaction must be required');
|
|
57
|
+
if (policy.artifact_policy.evidence_manifest !== 'required') fail('EVIDENCE_MANIFEST_REQUIRED', 'artifact_policy.evidence_manifest must be required');
|
|
58
|
+
string(policy.artifact_policy.retention_class, 'RETENTION_REQUIRED', 'artifact_policy.retention_class');
|
|
59
|
+
if (!ACTION_CONFIRMATIONS.has(policy.action_confirmation)) fail('INVALID_ACTION_CONFIRMATION', 'action_confirmation must be required or not-required-by-policy');
|
|
60
|
+
return freeze(canonicalize({ ...policy, allowed_domains }));
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/** Ensures task declarations cannot reach an adapter without an explicit C-10 policy. */
|
|
64
|
+
export function assertBrowserCapabilityForTask(task = {}) {
|
|
65
|
+
object(task, 'INVALID_TASK_CAPABILITY', 'task capability declaration');
|
|
66
|
+
if (task.requires_browser !== true) return null;
|
|
67
|
+
if (!task.browser_policy) fail('BROWSER_POLICY_REQUIRED', 'a browser task requires browser_policy before any adapter may be called');
|
|
68
|
+
const policy = validateBrowserPolicy(task.browser_policy);
|
|
69
|
+
if (task.task_id !== undefined && task.task_id !== policy.task_id) fail('TASK_POLICY_MISMATCH', 'task_id must match browser policy.task_id');
|
|
70
|
+
return policy;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function validateLaunchRequest(input, policy) {
|
|
74
|
+
object(input, 'INVALID_BROWSER_REQUEST', 'browser launch request');
|
|
75
|
+
string(input.browser_session_id, 'INVALID_BROWSER_REQUEST', 'browser_session_id');
|
|
76
|
+
string(input.task_id, 'INVALID_BROWSER_REQUEST', 'task_id');
|
|
77
|
+
if (input.task_id !== policy.task_id) fail('TASK_POLICY_MISMATCH', 'launch task_id must match policy.task_id');
|
|
78
|
+
const hostname = urlDomain(input.url);
|
|
79
|
+
if (!domainAllowed(hostname, policy.allowed_domains)) fail('DOMAIN_NOT_ALLOWED', 'requested URL hostname is outside policy allowed_domains', { hostname });
|
|
80
|
+
string(input.classification, 'MISSING_CLASSIFICATION', 'classification');
|
|
81
|
+
if (!CLASSIFICATIONS.has(input.classification)) fail('INVALID_CLASSIFICATION', 'classification is invalid');
|
|
82
|
+
if (input.classification === 'client-restricted') ref(input.tenant_ref, 'MISSING_TENANT_REF', 'tenant_ref');
|
|
83
|
+
if (policy.action_confirmation === 'required') ref(input.action_confirmation_ref, 'ACTION_CONFIRMATION_REQUIRED', 'action_confirmation_ref');
|
|
84
|
+
return freeze(canonicalize({ ...input, hostname }));
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Internal test-only adapter. It cannot perform I/O and is never injectable.
|
|
89
|
+
* Keeping construction inside BrowserCapability prevents a caller from using a
|
|
90
|
+
* label such as `fake-browser-adapter` to smuggle an I/O-capable adapter over
|
|
91
|
+
* the G3 boundary.
|
|
92
|
+
*/
|
|
93
|
+
class InternalFakeBrowserAdapter {
|
|
94
|
+
#launches = [];
|
|
95
|
+
get launches() { return freeze(canonicalize(this.#launches)); }
|
|
96
|
+
launch(preflight) {
|
|
97
|
+
this.#launches.push(canonicalize(preflight));
|
|
98
|
+
return freeze(canonicalize({ fake_launch_id: `fake-browser-launch-${this.#launches.length}` }));
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
export class BrowserCapability {
|
|
103
|
+
#adapter;
|
|
104
|
+
#issuedManifests = new WeakSet();
|
|
105
|
+
constructor(options = {}) {
|
|
106
|
+
object(options, 'INVALID_BROWSER_CAPABILITY_OPTIONS', 'browser capability options');
|
|
107
|
+
if (Object.hasOwn(options, 'adapter')) fail('EXTERNAL_ADAPTER_FORBIDDEN', 'G3 constructs its internal fake adapter and accepts no injected adapter');
|
|
108
|
+
this.#adapter = new InternalFakeBrowserAdapter();
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
get fakeLaunches() { return this.#adapter.launches; }
|
|
112
|
+
#issue(manifest) {
|
|
113
|
+
const issued = freeze(canonicalize(manifest));
|
|
114
|
+
this.#issuedManifests.add(issued);
|
|
115
|
+
return issued;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
preflight(input = {}) {
|
|
119
|
+
const policy = validateBrowserPolicy(input.policy);
|
|
120
|
+
const request = validateLaunchRequest(input, policy);
|
|
121
|
+
const manifestInput = {
|
|
122
|
+
schema_version: 1,
|
|
123
|
+
manifest_type: 'BrowserEvidenceManifest',
|
|
124
|
+
browser_session_id: request.browser_session_id,
|
|
125
|
+
task_id: request.task_id,
|
|
126
|
+
policy_ref: policy.policy_id,
|
|
127
|
+
mode: policy.mode,
|
|
128
|
+
tool: policy.tool,
|
|
129
|
+
allowed_domain: request.hostname,
|
|
130
|
+
profile_isolation: policy.profile_isolation,
|
|
131
|
+
classification: request.classification,
|
|
132
|
+
artifact_policy: policy.artifact_policy,
|
|
133
|
+
action_confirmation: policy.action_confirmation,
|
|
134
|
+
artifacts: [],
|
|
135
|
+
launch: { adapter: 'fake-browser-adapter', status: 'preflighted' },
|
|
136
|
+
};
|
|
137
|
+
if (request.tenant_ref !== undefined) manifestInput.tenant_ref = request.tenant_ref;
|
|
138
|
+
if (request.action_confirmation_ref !== undefined) manifestInput.action_confirmation_ref = request.action_confirmation_ref;
|
|
139
|
+
return this.#issue(manifestInput);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
launch(input = {}) {
|
|
143
|
+
const manifest = this.preflight(input);
|
|
144
|
+
const result = this.#adapter.launch(freeze(canonicalize({ policy_ref: manifest.policy_ref, browser_session_id: manifest.browser_session_id, task_id: manifest.task_id, url: input.url, hostname: manifest.allowed_domain, mode: manifest.mode, profile_isolation: manifest.profile_isolation })));
|
|
145
|
+
return this.#issue({ ...manifest, launch: { adapter: 'fake-browser-adapter', status: 'launched', fake_launch_ref: result.fake_launch_id } });
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
recordArtifact({ manifest, artifact } = {}) {
|
|
149
|
+
object(manifest, 'INVALID_BROWSER_MANIFEST', 'BrowserEvidenceManifest');
|
|
150
|
+
if (manifest.manifest_type !== 'BrowserEvidenceManifest' || manifest.schema_version !== 1) fail('INVALID_BROWSER_MANIFEST', 'manifest is not a supported BrowserEvidenceManifest');
|
|
151
|
+
if (!this.#issuedManifests.has(manifest)) fail('UNISSUED_BROWSER_MANIFEST', 'manifest must be emitted by this BrowserCapability preflight or launch');
|
|
152
|
+
object(artifact, 'INVALID_BROWSER_ARTIFACT', 'browser artifact');
|
|
153
|
+
for (const field of ['artifact_id', 'kind', 'sanitized_ref', 'redaction_status', 'retention_class']) string(artifact[field], 'INVALID_BROWSER_ARTIFACT', `artifact.${field}`);
|
|
154
|
+
if (!ARTIFACT_KINDS.has(artifact.kind)) fail('INVALID_BROWSER_ARTIFACT', 'artifact.kind is invalid');
|
|
155
|
+
if (artifact.redaction_status !== 'redacted') fail('ARTIFACT_NOT_REDACTED', 'browser artifact must be redacted before entering the manifest');
|
|
156
|
+
if (artifact.retention_class !== manifest.artifact_policy.retention_class) fail('RETENTION_MISMATCH', 'artifact retention_class must match browser policy');
|
|
157
|
+
const artifacts = [...manifest.artifacts];
|
|
158
|
+
if (artifacts.some((item) => item.artifact_id === artifact.artifact_id)) fail('DUPLICATE_BROWSER_ARTIFACT', 'artifact_id is already present in manifest');
|
|
159
|
+
artifacts.push(canonicalize(artifact));
|
|
160
|
+
const output = { ...manifest, artifacts };
|
|
161
|
+
delete output.manifest_hash;
|
|
162
|
+
output.manifest_hash = sha256(output);
|
|
163
|
+
return this.#issue(output);
|
|
164
|
+
}
|
|
165
|
+
}
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
2
|
+
|
|
3
|
+
export class ContractError extends Error {
|
|
4
|
+
constructor(code, message, details = {}) {
|
|
5
|
+
super(message);
|
|
6
|
+
this.name = 'ContractError';
|
|
7
|
+
this.code = code;
|
|
8
|
+
this.details = details;
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
const RFC3339 = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d+)?(?:Z|[+-]\d{2}:\d{2})$/;
|
|
13
|
+
const CLASSIFICATIONS = new Set(['internal', 'confidential', 'client-restricted', 'personal-restricted']);
|
|
14
|
+
const ACTOR_TYPES = new Set(['human', 'harness', 'service']);
|
|
15
|
+
const CONTRACT_TYPES = new Set(['installation', 'handoff', 'policy', 'task', 'review']);
|
|
16
|
+
|
|
17
|
+
function assertObject(value, code, label) {
|
|
18
|
+
if (!value || typeof value !== 'object' || Array.isArray(value)) {
|
|
19
|
+
throw new ContractError(code, `${label} must be an object`);
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function assertString(value, code, label) {
|
|
24
|
+
if (typeof value !== 'string' || value.trim() === '') {
|
|
25
|
+
throw new ContractError(code, `${label} must be a non-empty string`);
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Produces a JSON-compatible value whose object keys are recursively sorted.
|
|
31
|
+
* Arrays retain their declared order because order is semantically meaningful
|
|
32
|
+
* in contract artifacts.
|
|
33
|
+
*/
|
|
34
|
+
export function canonicalize(value, path = '$') {
|
|
35
|
+
if (value === null || typeof value === 'string' || typeof value === 'boolean') return value;
|
|
36
|
+
if (typeof value === 'number') {
|
|
37
|
+
if (!Number.isFinite(value)) throw new ContractError('NON_CANONICAL_VALUE', `${path} must be finite`);
|
|
38
|
+
return value;
|
|
39
|
+
}
|
|
40
|
+
if (Array.isArray(value)) return value.map((entry, index) => canonicalize(entry, `${path}[${index}]`));
|
|
41
|
+
if (typeof value === 'object') {
|
|
42
|
+
const output = {};
|
|
43
|
+
for (const key of Object.keys(value).sort()) {
|
|
44
|
+
if (value[key] === undefined) throw new ContractError('NON_CANONICAL_VALUE', `${path}.${key} is undefined`);
|
|
45
|
+
output[key] = canonicalize(value[key], `${path}.${key}`);
|
|
46
|
+
}
|
|
47
|
+
return output;
|
|
48
|
+
}
|
|
49
|
+
throw new ContractError('NON_CANONICAL_VALUE', `${path} is not JSON-compatible`);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export function canonicalSerialize(value) {
|
|
53
|
+
return JSON.stringify(canonicalize(value));
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export function sha256(value) {
|
|
57
|
+
const input = typeof value === 'string' ? value : canonicalSerialize(value);
|
|
58
|
+
return createHash('sha256').update(input, 'utf8').digest('hex');
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export function validateEvidenceEnvelope(envelope) {
|
|
62
|
+
assertObject(envelope, 'INVALID_ENVELOPE', 'envelope');
|
|
63
|
+
assertString(envelope.event_id, 'INVALID_ENVELOPE', 'event_id');
|
|
64
|
+
if (envelope.schema_version !== 1) throw new ContractError('UNSUPPORTED_SCHEMA_VERSION', 'schema_version must be 1');
|
|
65
|
+
assertString(envelope.occurred_at, 'INVALID_ENVELOPE', 'occurred_at');
|
|
66
|
+
if (!RFC3339.test(envelope.occurred_at)) throw new ContractError('INVALID_ENVELOPE', 'occurred_at must be RFC3339');
|
|
67
|
+
assertString(envelope.project_id, 'INVALID_ENVELOPE', 'project_id');
|
|
68
|
+
assertString(envelope.correlation_id, 'INVALID_ENVELOPE', 'correlation_id');
|
|
69
|
+
if (envelope.causation_id !== undefined) assertString(envelope.causation_id, 'INVALID_ENVELOPE', 'causation_id');
|
|
70
|
+
assertObject(envelope.actor, 'INVALID_ENVELOPE', 'actor');
|
|
71
|
+
if (!ACTOR_TYPES.has(envelope.actor.type)) throw new ContractError('INVALID_ENVELOPE', 'actor.type is invalid');
|
|
72
|
+
assertString(envelope.actor.id, 'INVALID_ENVELOPE', 'actor.id');
|
|
73
|
+
assertObject(envelope.contract_ref, 'INVALID_ENVELOPE', 'contract_ref');
|
|
74
|
+
if (!CONTRACT_TYPES.has(envelope.contract_ref.type)) throw new ContractError('INVALID_ENVELOPE', 'contract_ref.type is invalid');
|
|
75
|
+
assertString(envelope.contract_ref.id, 'INVALID_ENVELOPE', 'contract_ref.id');
|
|
76
|
+
assertString(envelope.contract_ref.version, 'INVALID_ENVELOPE', 'contract_ref.version');
|
|
77
|
+
if (!CLASSIFICATIONS.has(envelope.classification)) throw new ContractError('INVALID_ENVELOPE', 'classification is invalid');
|
|
78
|
+
if (envelope.classification === 'client-restricted') assertString(envelope.tenant_ref, 'MISSING_TENANT_REF', 'tenant_ref');
|
|
79
|
+
assertObject(envelope.payload, 'INVALID_ENVELOPE', 'payload');
|
|
80
|
+
if (!Array.isArray(envelope.evidence_refs) || !envelope.evidence_refs.every((ref) => typeof ref === 'string' && ref)) {
|
|
81
|
+
throw new ContractError('INVALID_ENVELOPE', 'evidence_refs must be an array of non-empty strings');
|
|
82
|
+
}
|
|
83
|
+
return Object.freeze(canonicalize(envelope));
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export function buildEvidenceEnvelope(input) {
|
|
87
|
+
const envelope = {
|
|
88
|
+
schema_version: 1,
|
|
89
|
+
evidence_refs: [],
|
|
90
|
+
...input,
|
|
91
|
+
};
|
|
92
|
+
if (envelope.causation_id === undefined) delete envelope.causation_id;
|
|
93
|
+
return validateEvidenceEnvelope(envelope);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
export function validateInvocationRequest(invocation) {
|
|
97
|
+
assertObject(invocation, 'INVALID_INVOCATION', 'invocation');
|
|
98
|
+
for (const field of ['invocation_id', 'attempt_id', 'action', 'provider_id', 'harness_id', 'tool_policy_ref', 'input_ref', 'expected_output_schema']) {
|
|
99
|
+
assertString(invocation[field], 'INVALID_INVOCATION', field);
|
|
100
|
+
}
|
|
101
|
+
assertObject(invocation.model_pin, 'INVALID_INVOCATION', 'model_pin');
|
|
102
|
+
assertString(invocation.model_pin.model_id, 'INVALID_INVOCATION', 'model_pin.model_id');
|
|
103
|
+
assertString(invocation.model_pin.catalog_snapshot_ref, 'INVALID_INVOCATION', 'model_pin.catalog_snapshot_ref');
|
|
104
|
+
if (invocation.model_pin.reasoning_configuration !== undefined && invocation.model_pin.reasoning_configuration !== null) {
|
|
105
|
+
assertString(invocation.model_pin.reasoning_configuration, 'INVALID_INVOCATION', 'model_pin.reasoning_configuration');
|
|
106
|
+
}
|
|
107
|
+
if (invocation.requested_tool_ids !== undefined && (!Array.isArray(invocation.requested_tool_ids) || !invocation.requested_tool_ids.every((id) => typeof id === 'string' && id))) {
|
|
108
|
+
throw new ContractError('INVALID_INVOCATION', 'requested_tool_ids must be an array of non-empty strings');
|
|
109
|
+
}
|
|
110
|
+
return Object.freeze(canonicalize(invocation));
|
|
111
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@chati/knowledge-context",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"private": true,
|
|
5
|
+
"description": "Scoped Company Brain reads and project-local learning candidate artifacts",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"exports": "./src/index.js",
|
|
8
|
+
"dependencies": {
|
|
9
|
+
"@chati/core": "0.1.0"
|
|
10
|
+
},
|
|
11
|
+
"scripts": {
|
|
12
|
+
"test": "node --test test/**/*.test.js"
|
|
13
|
+
},
|
|
14
|
+
"engines": {
|
|
15
|
+
"node": ">=20.0.0"
|
|
16
|
+
}
|
|
17
|
+
}
|