arkaos 4.13.2 → 4.14.1
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/VERSION +1 -1
- package/arka/SKILL.md +16 -0
- package/bin/arka-doctor +20 -1
- package/config/cognition/schedules.yaml +16 -0
- package/config/hooks/_lib/fastpath/engine.cjs +337 -0
- package/config/hooks/gate-manifest.json +741 -0
- package/config/hooks/post-tool-use.cjs +113 -0
- package/config/hooks/pre-tool-use.cjs +109 -0
- package/config/hooks/session-end.ps1 +40 -0
- package/config/hooks/session-end.sh +55 -0
- package/config/hooks/session-start.sh +25 -189
- package/config/hooks/subagent-stop.ps1 +40 -0
- package/config/hooks/subagent-stop.sh +56 -0
- package/config/mcp-policy.yaml +8 -0
- package/config/skills-curated.yaml +39 -0
- package/config/statusline.sh +57 -1
- package/core/cognition/capture/__pycache__/store.cpython-313.pyc +0 -0
- package/core/cognition/capture/store.py +7 -2
- package/core/evals/__pycache__/__init__.cpython-314.pyc +0 -0
- package/core/evals/__pycache__/sanitizer.cpython-314.pyc +0 -0
- package/core/evals/__pycache__/schema.cpython-314.pyc +0 -0
- package/core/evals/__pycache__/verdict_labels.cpython-314.pyc +0 -0
- package/core/forge/__pycache__/complexity.cpython-314.pyc +0 -0
- package/core/governance/__pycache__/evidence_checks.cpython-313.pyc +0 -0
- package/core/governance/__pycache__/judge.cpython-314.pyc +0 -0
- package/core/governance/__pycache__/leak_scanner.cpython-314.pyc +0 -0
- package/core/governance/__pycache__/qg_verdict.cpython-314.pyc +0 -0
- package/core/governance/__pycache__/routing_feedback.cpython-313.pyc +0 -0
- package/core/governance/__pycache__/routing_feedback.cpython-314.pyc +0 -0
- package/core/governance/__pycache__/routing_feedback_cli.cpython-313.pyc +0 -0
- package/core/governance/evidence_checks.py +42 -7
- package/core/governance/routing_feedback.py +229 -0
- package/core/governance/routing_feedback_cli.py +49 -0
- package/core/hooks/__pycache__/_shared.cpython-312.pyc +0 -0
- package/core/hooks/__pycache__/gate_manifest.cpython-313.pyc +0 -0
- package/core/hooks/__pycache__/post_tool_use.cpython-312.pyc +0 -0
- package/core/hooks/__pycache__/post_tool_use.cpython-313.pyc +0 -0
- package/core/hooks/__pycache__/pre_tool_use.cpython-312.pyc +0 -0
- package/core/hooks/__pycache__/session_end.cpython-313.pyc +0 -0
- package/core/hooks/__pycache__/session_start.cpython-312.pyc +0 -0
- package/core/hooks/__pycache__/session_start.cpython-313.pyc +0 -0
- package/core/hooks/__pycache__/stop.cpython-313.pyc +0 -0
- package/core/hooks/__pycache__/stop.cpython-314.pyc +0 -0
- package/core/hooks/__pycache__/subagent_stop.cpython-313.pyc +0 -0
- package/core/hooks/__pycache__/subagent_stop.cpython-314.pyc +0 -0
- package/core/hooks/__pycache__/user_prompt_submit.cpython-313.pyc +0 -0
- package/core/hooks/__pycache__/user_prompt_submit.cpython-314.pyc +0 -0
- package/core/hooks/gate_manifest.py +366 -0
- package/core/hooks/post_tool_use.py +5 -2
- package/core/hooks/session_end.py +127 -0
- package/core/hooks/session_start.py +322 -0
- package/core/hooks/stop.py +69 -0
- package/core/hooks/subagent_stop.py +186 -0
- package/core/hooks/user_prompt_submit.py +10 -6
- package/core/knowledge/__pycache__/embedding_backends.cpython-313.pyc +0 -0
- package/core/knowledge/__pycache__/embedding_backends.cpython-314.pyc +0 -0
- package/core/knowledge/__pycache__/pattern_cards.cpython-313.pyc +0 -0
- package/core/knowledge/__pycache__/pattern_cards.cpython-314.pyc +0 -0
- package/core/knowledge/__pycache__/recipes.cpython-314.pyc +0 -0
- package/core/knowledge/__pycache__/vector_store.cpython-313.pyc +0 -0
- package/core/knowledge/__pycache__/vector_store.cpython-314.pyc +0 -0
- package/core/knowledge/embedding_backends.py +278 -0
- package/core/knowledge/pattern_cards.py +49 -1
- package/core/knowledge/vector_store.py +14 -2
- package/core/memory/__pycache__/semantic_store.cpython-312.pyc +0 -0
- package/core/memory/__pycache__/semantic_store.cpython-313.pyc +0 -0
- package/core/memory/__pycache__/semantic_store.cpython-314.pyc +0 -0
- package/core/memory/__pycache__/turn_capture.cpython-313.pyc +0 -0
- package/core/memory/__pycache__/turn_capture.cpython-314.pyc +0 -0
- package/core/memory/semantic_store.py +301 -0
- package/core/memory/turn_capture.py +277 -0
- package/core/registry/__pycache__/generator.cpython-312.pyc +0 -0
- package/core/runtime/__pycache__/claude_code.cpython-312.pyc +0 -0
- package/core/runtime/__pycache__/codex_cli.cpython-312.pyc +0 -0
- package/core/runtime/__pycache__/cost_governor.cpython-314.pyc +0 -0
- package/core/runtime/__pycache__/gemini_cli.cpython-312.pyc +0 -0
- package/core/runtime/__pycache__/llm_cost_telemetry.cpython-312.pyc +0 -0
- package/core/runtime/__pycache__/mcp_telemetry.cpython-312.pyc +0 -0
- package/core/shared/__pycache__/decay.cpython-313.pyc +0 -0
- package/core/shared/__pycache__/decay.cpython-314.pyc +0 -0
- package/core/shared/__pycache__/sqlite_recovery.cpython-312.pyc +0 -0
- package/core/shared/__pycache__/sqlite_recovery.cpython-313.pyc +0 -0
- package/core/shared/__pycache__/sqlite_recovery.cpython-314.pyc +0 -0
- package/core/shared/decay.py +94 -0
- package/core/shared/sqlite_recovery.py +266 -0
- package/core/synapse/__pycache__/agent_experiences_layer.cpython-313.pyc +0 -0
- package/core/synapse/__pycache__/agent_experiences_layer.cpython-314.pyc +0 -0
- package/core/synapse/__pycache__/engine.cpython-312.pyc +0 -0
- package/core/synapse/__pycache__/engine.cpython-313.pyc +0 -0
- package/core/synapse/__pycache__/engine.cpython-314.pyc +0 -0
- package/core/synapse/__pycache__/layers.cpython-312.pyc +0 -0
- package/core/synapse/__pycache__/pattern_library_layer.cpython-314.pyc +0 -0
- package/core/synapse/__pycache__/recipe_layer.cpython-313.pyc +0 -0
- package/core/synapse/__pycache__/recipe_layer.cpython-314.pyc +0 -0
- package/core/synapse/__pycache__/routing_feedback_layer.cpython-313.pyc +0 -0
- package/core/synapse/__pycache__/routing_feedback_layer.cpython-314.pyc +0 -0
- package/core/synapse/__pycache__/session_memory_layer.cpython-312.pyc +0 -0
- package/core/synapse/__pycache__/session_memory_layer.cpython-313.pyc +0 -0
- package/core/synapse/__pycache__/session_memory_layer.cpython-314.pyc +0 -0
- package/core/synapse/agent_experiences_layer.py +42 -3
- package/core/synapse/engine.py +13 -0
- package/core/synapse/recipe_layer.py +27 -4
- package/core/synapse/routing_feedback_layer.py +132 -0
- package/core/synapse/session_memory_layer.py +152 -0
- package/core/workflow/__pycache__/__init__.cpython-312.pyc +0 -0
- package/core/workflow/__pycache__/design_authorization.cpython-314.pyc +0 -0
- package/core/workflow/__pycache__/frontend_gate.cpython-314.pyc +0 -0
- package/core/workflow/__pycache__/specialist_enforcer.cpython-314.pyc +0 -0
- package/installer/adapters/claude-code.js +37 -0
- package/installer/claude-plugins.js +6 -0
- package/installer/cli.js +23 -2
- package/installer/config-seed.js +25 -16
- package/installer/doctor.js +116 -1
- package/installer/hook-lib.js +28 -0
- package/installer/index.js +48 -153
- package/installer/mcp-runner.js +108 -0
- package/installer/skill-deploy.js +189 -0
- package/installer/skills-mode.js +64 -0
- package/installer/update.js +55 -79
- package/knowledge/commands-registry.json.bak +171 -4
- package/knowledge/skills-manifest.json +2337 -0
- package/mcps/arka-prompts/commands.py +451 -0
- package/mcps/arka-prompts/pyproject.toml +15 -0
- package/mcps/arka-prompts/server.py +156 -0
- package/mcps/arka-tools/__pycache__/server.cpython-313.pyc +0 -0
- package/mcps/arka-tools/pyproject.toml +10 -0
- package/mcps/arka-tools/server.py +319 -0
- package/mcps/profiles/base.json +19 -0
- package/mcps/profiles/brand.json +19 -0
- package/mcps/profiles/comms.json +13 -0
- package/mcps/profiles/content.json +20 -0
- package/mcps/profiles/ecommerce.json +13 -0
- package/mcps/profiles/full-stack.json +15 -0
- package/mcps/profiles/laravel.json +12 -0
- package/mcps/profiles/nextjs.json +13 -0
- package/mcps/profiles/nuxt.json +13 -0
- package/mcps/profiles/react.json +12 -0
- package/mcps/profiles/vue.json +12 -0
- package/mcps/registry.json +423 -0
- package/mcps/scripts/apply-mcps.sh +282 -0
- package/mcps/stacks/laravel-packages.json +49 -0
- package/mcps/stacks/react-packages.json +58 -0
- package/package.json +2 -1
- package/pyproject.toml +9 -1
- package/scripts/__pycache__/marketplace_gen.cpython-313.pyc +0 -0
- package/scripts/__pycache__/synapse-bridge.cpython-314.pyc +0 -0
- package/scripts/marketplace_gen.py +310 -0
- package/scripts/tools/__pycache__/docs_stats.cpython-313.pyc +0 -0
- package/scripts/tools/__pycache__/skill_budget.cpython-313.pyc +0 -0
- package/scripts/tools/docs_stats.py +6 -4
- package/scripts/tools/skill_budget.py +209 -0
package/VERSION
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
4.
|
|
1
|
+
4.14.1
|
package/arka/SKILL.md
CHANGED
|
@@ -220,6 +220,22 @@ Users don't need to memorize commands. Just describe what you need:
|
|
|
220
220
|
English prompt, then dispatches back through `/do`. A request that is
|
|
221
221
|
already specific skips refinement.
|
|
222
222
|
|
|
223
|
+
### Skill Not Installed (curated default, F2-7c)
|
|
224
|
+
|
|
225
|
+
When /do resolves to a skill that is not installed (curated default;
|
|
226
|
+
the long tail lives in the department plugins), the Skill tool fails.
|
|
227
|
+
NEVER answer as a generic assistant or improvise the skill: look the
|
|
228
|
+
slug up in `knowledge/skills-manifest.json` and reply — in the user's
|
|
229
|
+
language — with the actionable install hint:
|
|
230
|
+
|
|
231
|
+
```
|
|
232
|
+
The <slug> skill is not in the installed curated set.
|
|
233
|
+
Install the department pack: /plugin install arkaos-<dept>@arkaos
|
|
234
|
+
Or restore the full set: npx arkaos update --skills full
|
|
235
|
+
```
|
|
236
|
+
|
|
237
|
+
Fail loud and actionable, never silently.
|
|
238
|
+
|
|
223
239
|
### Squad Routing (MUST)
|
|
224
240
|
|
|
225
241
|
EVERY request routes through the appropriate department squad. ArkaOS never responds
|
package/bin/arka-doctor
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
# ============================================================================
|
|
3
3
|
# ARKA OS — Doctor (Health Check System)
|
|
4
4
|
# Usage: arka doctor [--fix] [--json]
|
|
5
|
-
#
|
|
5
|
+
# 18 modular checks: fail = critical, warn = advisory
|
|
6
6
|
# ============================================================================
|
|
7
7
|
set -euo pipefail
|
|
8
8
|
|
|
@@ -376,6 +376,25 @@ for hf_name in hyperframes hyperframes-core; do
|
|
|
376
376
|
done
|
|
377
377
|
run_check "hyperframes-skills" "warn" "$HF_DESC" "$HF_OK" "Run /content video-setup in Claude Code"
|
|
378
378
|
|
|
379
|
+
# ─── Check 18: SQLite corrupt-store backups (self-heal F1-D1) ──────────────
|
|
380
|
+
# Parity with installer/doctor.js `sqlite-corrupt-backups`: same name,
|
|
381
|
+
# same pattern, warn-only. A <db>.corrupt-<ts>.bak means a store healed
|
|
382
|
+
# itself — the operator should inspect, then delete the backup.
|
|
383
|
+
BAK_OK=true
|
|
384
|
+
BAK_DESC="No self-healed SQLite stores awaiting inspection"
|
|
385
|
+
BAK_LIST=""
|
|
386
|
+
if [ -d "$HOME/.arkaos" ]; then
|
|
387
|
+
# `|| true`: set -euo pipefail must not kill the doctor when find
|
|
388
|
+
# errors or head closes the pipe early — absence of backups is fine.
|
|
389
|
+
BAK_LIST=$(find "$HOME/.arkaos" -name "*.corrupt-*.bak" -type f 2>/dev/null | head -3 || true)
|
|
390
|
+
fi
|
|
391
|
+
if [ -n "$BAK_LIST" ]; then
|
|
392
|
+
BAK_OK=false
|
|
393
|
+
BAK_FIRST=$(echo "$BAK_LIST" | head -1)
|
|
394
|
+
BAK_DESC="Self-healed store backup(s) found (e.g. ${BAK_FIRST##*/})"
|
|
395
|
+
fi
|
|
396
|
+
run_check "sqlite-corrupt-backups" "warn" "$BAK_DESC" "$BAK_OK" "Inspect then delete ~/.arkaos/**/*.corrupt-*.bak"
|
|
397
|
+
|
|
379
398
|
# ─── Output ────────────────────────────────────────────────────────────────
|
|
380
399
|
TOTAL=$((PASS_COUNT + WARN_COUNT + FAIL_COUNT))
|
|
381
400
|
|
|
@@ -20,6 +20,22 @@ schedules:
|
|
|
20
20
|
max_retries: 2
|
|
21
21
|
timeout_minutes: 60
|
|
22
22
|
|
|
23
|
+
session-memory-maintenance:
|
|
24
|
+
command: session-memory-maintenance
|
|
25
|
+
# F1-A4 (memory/learning reform): full embedding backfill, retention
|
|
26
|
+
# prune and VACUUM for ~/.arkaos/session-memory.db. The per-turn
|
|
27
|
+
# Stop-hook worker only amortises a small slice of this; the nightly
|
|
28
|
+
# job finishes the backlog. Cheap and idempotent; respects
|
|
29
|
+
# memory.sessionMemory + ARKA_SESSION_MEMORY=0.
|
|
30
|
+
python_module: "core.memory.turn_capture"
|
|
31
|
+
module_args: ["maintenance"]
|
|
32
|
+
time: "03:30"
|
|
33
|
+
timezone: auto
|
|
34
|
+
enabled: true
|
|
35
|
+
retry_on_fail: false
|
|
36
|
+
max_retries: 0
|
|
37
|
+
timeout_minutes: 20
|
|
38
|
+
|
|
23
39
|
research:
|
|
24
40
|
command: research
|
|
25
41
|
prompt_file: "~/.arkaos/cognition/prompts/research.md"
|
|
@@ -0,0 +1,337 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* ArkaOS hook fast-path engine (F2-6) — pure decision logic, no I/O
|
|
4
|
+
* beyond what the caller injects. Consumed by pre-tool-use.cjs and
|
|
5
|
+
* post-tool-use.cjs; exercised directly by
|
|
6
|
+
* tests/installer/gate-manifest.test.js against the SAME corpora the
|
|
7
|
+
* pytest side runs against the real Python functions.
|
|
8
|
+
*
|
|
9
|
+
* Security invariant #0: this engine has NO deny path. Every decision
|
|
10
|
+
* is either {action:"fast-allow"} on a closed, corpus-proven set, or
|
|
11
|
+
* {action:"delegate"} to the bash->Python chain. A bug here degrades
|
|
12
|
+
* to latency, never to a skipped deny.
|
|
13
|
+
*
|
|
14
|
+
* Every constant comes from the injected manifest
|
|
15
|
+
* (config/gate-manifest.json, generated by core/hooks/gate_manifest.py
|
|
16
|
+
* from the real Python gate constants). No constant may be declared in
|
|
17
|
+
* this file — that is exactly the drift the manifest exists to kill.
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
const SCHEMA_VERSION = 1;
|
|
21
|
+
|
|
22
|
+
/** Python truthiness — NOT JS Boolean(): [] and {} are falsy in Python,
|
|
23
|
+
* "false" (non-empty string) is truthy in both. flow_enforcer coerces
|
|
24
|
+
* hooks.hardEnforcement with bool(), so the shim must match. */
|
|
25
|
+
function pythonTruthy(value) {
|
|
26
|
+
if (Array.isArray(value)) return value.length > 0;
|
|
27
|
+
if (value !== null && typeof value === "object") {
|
|
28
|
+
return Object.keys(value).length > 0;
|
|
29
|
+
}
|
|
30
|
+
return Boolean(value);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/** datetime.now(timezone.utc).isoformat() shape: microseconds + +00:00.
|
|
34
|
+
* JS has ms precision; pad to 6 digits — still fromisoformat-parseable. */
|
|
35
|
+
function pyIsoNow(now) {
|
|
36
|
+
const d = now || new Date();
|
|
37
|
+
const pad = (n, w) => String(n).padStart(w, "0");
|
|
38
|
+
const micros = pad(d.getUTCMilliseconds(), 3) + "000";
|
|
39
|
+
return (
|
|
40
|
+
`${d.getUTCFullYear()}-${pad(d.getUTCMonth() + 1, 2)}-` +
|
|
41
|
+
`${pad(d.getUTCDate(), 2)}T${pad(d.getUTCHours(), 2)}:` +
|
|
42
|
+
`${pad(d.getUTCMinutes(), 2)}:${pad(d.getUTCSeconds(), 2)}.` +
|
|
43
|
+
`${micros}+00:00`
|
|
44
|
+
);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function validateManifest(manifest) {
|
|
48
|
+
return Boolean(
|
|
49
|
+
manifest &&
|
|
50
|
+
manifest.schema_version === SCHEMA_VERSION &&
|
|
51
|
+
manifest.tools && manifest.bash && manifest.post &&
|
|
52
|
+
manifest.telemetry && manifest.io_contract
|
|
53
|
+
);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function compileRegex(entry) {
|
|
57
|
+
return new RegExp(entry.js, entry.flags || "");
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/** Mirror of flow_enforcer.bash_is_effect: blacklist anywhere first,
|
|
61
|
+
* strip VAR=val prefixes, whitelist first token, unknown → effect. */
|
|
62
|
+
function bashIsEffect(command, manifest) {
|
|
63
|
+
if (!command || !command.trim()) return false;
|
|
64
|
+
const stripped = command.trim();
|
|
65
|
+
// QG B1 (redo 1): JS \s / trim / split treat U+FEFF (and other
|
|
66
|
+
// non-ASCII whitespace) as separators; Python's re \s and str.split
|
|
67
|
+
// do NOT for U+FEFF — a token like "FOO=1<U+FEFF>grep" is ONE unknown
|
|
68
|
+
// token (EFFECT) to Python but splits to a whitelisted "grep" here,
|
|
69
|
+
// a skipped deny under hard enforcement. Any whitespace-class char
|
|
70
|
+
// outside the ASCII set forces EFFECT → the shim delegates and
|
|
71
|
+
// Python classifies with its own semantics (safe: no deny path).
|
|
72
|
+
if (/[^\S \t\n\r\f\v]/.test(stripped)) return true;
|
|
73
|
+
for (const entry of manifest.bash.effect_patterns) {
|
|
74
|
+
if (compileRegex(entry).test(stripped)) return true;
|
|
75
|
+
}
|
|
76
|
+
let rest = stripped;
|
|
77
|
+
let first = rest.split(/\s+/, 1)[0] || "";
|
|
78
|
+
while (first.includes("=")) {
|
|
79
|
+
const idx = rest.search(/\s/);
|
|
80
|
+
if (idx < 0) return false;
|
|
81
|
+
rest = rest.slice(idx).trim();
|
|
82
|
+
first = rest.split(/\s+/, 1)[0] || "";
|
|
83
|
+
if (!rest) return false;
|
|
84
|
+
}
|
|
85
|
+
if (!first) return false;
|
|
86
|
+
if (manifest.bash.discovery_first_tokens.includes(first)) return false;
|
|
87
|
+
return true;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/** Strict session-id allowlist (core/shared/safe_session_id.py):
|
|
91
|
+
* charset pattern + dot-only rejection. null means "never build a
|
|
92
|
+
* filesystem path from this id". */
|
|
93
|
+
function safeSessionId(sid, manifest) {
|
|
94
|
+
if (!sid || typeof sid !== "string") return null;
|
|
95
|
+
if (!compileRegex(manifest.session_id).test(sid)) return null;
|
|
96
|
+
if (manifest.session_id.reject_dot_only && /^\.+$/.test(sid)) return null;
|
|
97
|
+
return sid;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function readJsonFile(fs, filePath) {
|
|
101
|
+
// {state: "missing" | "corrupt" | "ok", data}
|
|
102
|
+
let raw;
|
|
103
|
+
try {
|
|
104
|
+
raw = fs.readFileSync(filePath, "utf8");
|
|
105
|
+
} catch {
|
|
106
|
+
return { state: "missing", data: null };
|
|
107
|
+
}
|
|
108
|
+
try {
|
|
109
|
+
const data = JSON.parse(raw);
|
|
110
|
+
return { state: "ok", data };
|
|
111
|
+
} catch {
|
|
112
|
+
return { state: "corrupt", data: null };
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/** cost_governor semantics: budget active iff the section is a dict and
|
|
117
|
+
* any cap key parses as float > 0. Missing/corrupt config → {} → no cap
|
|
118
|
+
* (cost_governor.py:68-86). */
|
|
119
|
+
function budgetActive(configResult, manifest) {
|
|
120
|
+
if (configResult.state !== "ok") return false;
|
|
121
|
+
const data = configResult.data;
|
|
122
|
+
const section =
|
|
123
|
+
data && typeof data === "object" && !Array.isArray(data)
|
|
124
|
+
? data[manifest.flags.budget.section]
|
|
125
|
+
: null;
|
|
126
|
+
if (!section || typeof section !== "object" || Array.isArray(section)) {
|
|
127
|
+
return false;
|
|
128
|
+
}
|
|
129
|
+
for (const key of manifest.flags.budget.cap_keys) {
|
|
130
|
+
const value = Number(section[key]);
|
|
131
|
+
if (Number.isFinite(value) && value > 0) return true;
|
|
132
|
+
}
|
|
133
|
+
return false;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/** flow_enforcer._feature_flag_on: missing file → false, corrupt →
|
|
137
|
+
* false, else python-truthy of hooks.hardEnforcement. */
|
|
138
|
+
function hardEnforcementOn(configResult, manifest) {
|
|
139
|
+
if (configResult.state !== "ok") return false;
|
|
140
|
+
const [section, key] = manifest.flags.hardEnforcement.path;
|
|
141
|
+
const data = configResult.data;
|
|
142
|
+
const hooks =
|
|
143
|
+
data && typeof data === "object" && !Array.isArray(data)
|
|
144
|
+
? data[section]
|
|
145
|
+
: null;
|
|
146
|
+
if (!hooks || typeof hooks !== "object" || Array.isArray(hooks)) {
|
|
147
|
+
return false;
|
|
148
|
+
}
|
|
149
|
+
return pythonTruthy(hooks[key]);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
function telemetryLine(prefix, template, now) {
|
|
153
|
+
return JSON.stringify({ ...prefix, ...template }) + "\n";
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* PreToolUse decision. Returns
|
|
158
|
+
* {action:"fast-allow", writes:[{path, line}]} — exit 0, empty stdout
|
|
159
|
+
* {action:"delegate", reason} — spawn the sibling .sh
|
|
160
|
+
* ctx: {homeDir, config: readJsonFile result for ~/.arkaos/config.json,
|
|
161
|
+
* now?: Date}
|
|
162
|
+
*/
|
|
163
|
+
function decidePre(payload, manifest, ctx) {
|
|
164
|
+
if (!validateManifest(manifest)) {
|
|
165
|
+
return { action: "delegate", reason: "manifest-invalid" };
|
|
166
|
+
}
|
|
167
|
+
const toolName = String(payload.tool_name || "");
|
|
168
|
+
const tools = manifest.tools;
|
|
169
|
+
|
|
170
|
+
// P3 — research tools can deny (2nd violation) + vault rglob nudge.
|
|
171
|
+
if (tools.research_external.includes(toolName)) {
|
|
172
|
+
return { action: "delegate", reason: "research-gated" };
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
// P4 — not flow-gated: no Python gate ever denies these; the only
|
|
176
|
+
// parity duty is the kb_first telemetry append (research_gate
|
|
177
|
+
// record_telemetry runs for every tool).
|
|
178
|
+
if (!tools.flow_gated.includes(toolName)) {
|
|
179
|
+
return {
|
|
180
|
+
action: "fast-allow",
|
|
181
|
+
writes: [kbFirstWrite(payload, manifest, ctx, toolName)],
|
|
182
|
+
};
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
// P5/P6/P7 — Bash: discovery commands are allow-with-telemetry before
|
|
186
|
+
// any flag/bypass check (flow_enforcer.py:456-463); budget must be
|
|
187
|
+
// provably inactive because cost_governor runs first for flow-gated
|
|
188
|
+
// tools and can warn/deny.
|
|
189
|
+
if (toolName === "Bash") {
|
|
190
|
+
const command = String(
|
|
191
|
+
(payload.tool_input && payload.tool_input.command) || ""
|
|
192
|
+
);
|
|
193
|
+
if (!bashIsEffect(command, manifest) && !budgetActive(ctx.config, manifest)) {
|
|
194
|
+
return {
|
|
195
|
+
action: "fast-allow",
|
|
196
|
+
writes: [
|
|
197
|
+
kbFirstWrite(payload, manifest, ctx, toolName),
|
|
198
|
+
enforcementWrite(payload, manifest, ctx, toolName),
|
|
199
|
+
],
|
|
200
|
+
};
|
|
201
|
+
}
|
|
202
|
+
return { action: "delegate", reason: "bash-effect-or-budget" };
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
// P7/P8 — Write/Edit/MultiEdit/NotebookEdit/Task/Skill: specialist +
|
|
206
|
+
// frontend + flow chains with decision-bearing state writes.
|
|
207
|
+
return { action: "delegate", reason: "flow-gated" };
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
function kbFirstWrite(payload, manifest, ctx, toolName) {
|
|
211
|
+
const t = manifest.telemetry;
|
|
212
|
+
return {
|
|
213
|
+
path: `${ctx.homeDir}/${manifest.home_paths.telemetry_kb_first}`,
|
|
214
|
+
line: telemetryLine(
|
|
215
|
+
{
|
|
216
|
+
ts: pyIsoNow(ctx.now),
|
|
217
|
+
session_id: String(payload.session_id || ""),
|
|
218
|
+
tool: toolName,
|
|
219
|
+
},
|
|
220
|
+
t.kb_first_template
|
|
221
|
+
),
|
|
222
|
+
};
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
function enforcementWrite(payload, manifest, ctx, toolName) {
|
|
226
|
+
const t = manifest.telemetry;
|
|
227
|
+
return {
|
|
228
|
+
path: `${ctx.homeDir}/${manifest.home_paths.telemetry_enforcement}`,
|
|
229
|
+
line: telemetryLine(
|
|
230
|
+
{
|
|
231
|
+
ts: pyIsoNow(ctx.now),
|
|
232
|
+
session_id: String(payload.session_id || ""),
|
|
233
|
+
tool: toolName,
|
|
234
|
+
cwd: String(payload.cwd || ""),
|
|
235
|
+
},
|
|
236
|
+
t.enforcement_template
|
|
237
|
+
),
|
|
238
|
+
};
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
/** Flow-auth freshness (Q6): auth file exists, valid JSON, numeric
|
|
242
|
+
* confirmed_ts within TTL. Anything else → not fresh → delegate. */
|
|
243
|
+
function flowAuthFresh(sid, manifest, ctx) {
|
|
244
|
+
const safe = safeSessionId(sid, manifest);
|
|
245
|
+
if (!safe) return false;
|
|
246
|
+
const dirs = manifest.state_dirs;
|
|
247
|
+
const override = (ctx.env[dirs.flow_auth_env_override] || "").trim();
|
|
248
|
+
const base = override ||
|
|
249
|
+
`${dirs.tmp_base_posix}/${dirs.flow_auth_dirname}`;
|
|
250
|
+
const result = readJsonFile(ctx.fs, `${base}/${safe}.json`);
|
|
251
|
+
if (result.state !== "ok" || !result.data) return false;
|
|
252
|
+
const ts = result.data.confirmed_ts;
|
|
253
|
+
if (typeof ts !== "number" || !Number.isFinite(ts)) return false;
|
|
254
|
+
const nowSeconds = (ctx.now ? ctx.now.getTime() : Date.now()) / 1000;
|
|
255
|
+
return nowSeconds - ts <= manifest.numbers.auth_ttl_seconds;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
/**
|
|
259
|
+
* PostToolUse decision. Returns
|
|
260
|
+
* {action:"fast-exit", stdout:"{}", writes:[...]} — mcp telemetry only
|
|
261
|
+
* {action:"delegate", reason}
|
|
262
|
+
* ctx: {homeDir, config, env, fs, now?: Date}
|
|
263
|
+
*/
|
|
264
|
+
function decidePost(payload, manifest, ctx) {
|
|
265
|
+
if (!validateManifest(manifest)) {
|
|
266
|
+
return { action: "delegate", reason: "manifest-invalid" };
|
|
267
|
+
}
|
|
268
|
+
const toolName = String(payload.tool_name || "");
|
|
269
|
+
|
|
270
|
+
// Q3/Q4 — plan approval (G2) + subagent tracking live in Python.
|
|
271
|
+
if (manifest.tools.post_delegate_always.includes(toolName)) {
|
|
272
|
+
return { action: "delegate", reason: "post-stateful-tool" };
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
// Q5 — error path: gotchas, violations, cognition capture, metrics.
|
|
276
|
+
const exitCode = String(payload.exit_code ?? "") || "0";
|
|
277
|
+
if (!manifest.post.success_exit_codes.includes(exitCode)) {
|
|
278
|
+
return { action: "delegate", reason: "error-exit-code" };
|
|
279
|
+
}
|
|
280
|
+
const toolOutput = String(payload.tool_output ?? "");
|
|
281
|
+
if (compileRegex(manifest.post.error_trigger).test(toolOutput)) {
|
|
282
|
+
return { action: "delegate", reason: "error-trigger" };
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
// Q6/Q7 — the only remaining Python work on this path is the flow-auth
|
|
286
|
+
// confirm rescan. Redundant when enforcement is off (the pre-side flag
|
|
287
|
+
// check short-circuits before ever reading auth) or when a confirmed
|
|
288
|
+
// auth is fresh; load-bearing otherwise (GRACE_CAP exhaustion) →
|
|
289
|
+
// delegate.
|
|
290
|
+
const sid = String(payload.session_id || "");
|
|
291
|
+
const enforcementOff =
|
|
292
|
+
ctx.config.state !== "missing"
|
|
293
|
+
? !hardEnforcementOn(ctx.config, manifest)
|
|
294
|
+
: true; // missing config file → flag definitively false
|
|
295
|
+
if (!enforcementOff && !flowAuthFresh(sid, manifest, ctx)) {
|
|
296
|
+
return { action: "delegate", reason: "flow-auth-not-fresh" };
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
const writes = [];
|
|
300
|
+
const prefix = manifest.telemetry.mcp_prefix;
|
|
301
|
+
if (toolName.startsWith(prefix)) {
|
|
302
|
+
const rest = toolName.slice(prefix.length);
|
|
303
|
+
const sep = rest.indexOf("__");
|
|
304
|
+
const server = sep > 0 ? rest.slice(0, sep) : "";
|
|
305
|
+
const tool = sep > 0 ? rest.slice(sep + 2) : "";
|
|
306
|
+
if (server && tool) {
|
|
307
|
+
writes.push({
|
|
308
|
+
path: `${ctx.homeDir}/${manifest.home_paths.telemetry_mcp}`,
|
|
309
|
+
line: JSON.stringify({
|
|
310
|
+
ts: pyIsoNow(ctx.now),
|
|
311
|
+
server,
|
|
312
|
+
tool,
|
|
313
|
+
session: sid,
|
|
314
|
+
}) + "\n",
|
|
315
|
+
});
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
return {
|
|
319
|
+
action: "fast-exit",
|
|
320
|
+
stdout: manifest.io_contract.post.success_stdout,
|
|
321
|
+
writes,
|
|
322
|
+
};
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
module.exports = {
|
|
326
|
+
bashIsEffect,
|
|
327
|
+
budgetActive,
|
|
328
|
+
decidePost,
|
|
329
|
+
decidePre,
|
|
330
|
+
flowAuthFresh,
|
|
331
|
+
hardEnforcementOn,
|
|
332
|
+
pyIsoNow,
|
|
333
|
+
pythonTruthy,
|
|
334
|
+
readJsonFile,
|
|
335
|
+
safeSessionId,
|
|
336
|
+
validateManifest,
|
|
337
|
+
};
|