@personaxis/core 0.11.0
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/LICENSE +21 -0
- package/dist/agent.d.ts +105 -0
- package/dist/agent.js +429 -0
- package/dist/appraisal.d.ts +196 -0
- package/dist/appraisal.js +159 -0
- package/dist/approval.d.ts +42 -0
- package/dist/approval.js +71 -0
- package/dist/blackboard.d.ts +86 -0
- package/dist/blackboard.js +139 -0
- package/dist/compile/assemble.d.ts +44 -0
- package/dist/compile/assemble.js +338 -0
- package/dist/compile/dist.d.ts +30 -0
- package/dist/compile/dist.js +66 -0
- package/dist/compile/faithfulness.d.ts +47 -0
- package/dist/compile/faithfulness.js +112 -0
- package/dist/compile/index.d.ts +10 -0
- package/dist/compile/index.js +10 -0
- package/dist/compile/targets.d.ts +62 -0
- package/dist/compile/targets.js +114 -0
- package/dist/config-layers.d.ts +42 -0
- package/dist/config-layers.js +59 -0
- package/dist/config-scan.d.ts +30 -0
- package/dist/config-scan.js +118 -0
- package/dist/context.d.ts +67 -0
- package/dist/context.js +178 -0
- package/dist/envelopes.d.ts +48 -0
- package/dist/envelopes.js +131 -0
- package/dist/events.d.ts +135 -0
- package/dist/events.js +19 -0
- package/dist/evolution-view.d.ts +43 -0
- package/dist/evolution-view.js +67 -0
- package/dist/generated/version.d.ts +1 -0
- package/dist/generated/version.js +2 -0
- package/dist/governance.d.ts +108 -0
- package/dist/governance.js +204 -0
- package/dist/heuristic-appraiser.d.ts +13 -0
- package/dist/heuristic-appraiser.js +48 -0
- package/dist/hooks.d.ts +52 -0
- package/dist/hooks.js +122 -0
- package/dist/index.d.ts +53 -0
- package/dist/index.js +53 -0
- package/dist/injection.d.ts +48 -0
- package/dist/injection.js +141 -0
- package/dist/live-sync.d.ts +60 -0
- package/dist/live-sync.js +85 -0
- package/dist/llm-appraiser.d.ts +31 -0
- package/dist/llm-appraiser.js +143 -0
- package/dist/lock.d.ts +26 -0
- package/dist/lock.js +99 -0
- package/dist/loop.d.ts +52 -0
- package/dist/loop.js +253 -0
- package/dist/memory-kinds.d.ts +67 -0
- package/dist/memory-kinds.js +106 -0
- package/dist/memory.d.ts +123 -0
- package/dist/memory.js +228 -0
- package/dist/model-config.d.ts +60 -0
- package/dist/model-config.js +102 -0
- package/dist/persona-theme.d.ts +57 -0
- package/dist/persona-theme.js +183 -0
- package/dist/persona.d.ts +88 -0
- package/dist/persona.js +93 -0
- package/dist/ports/index.d.ts +81 -0
- package/dist/ports/index.js +50 -0
- package/dist/provenance.d.ts +63 -0
- package/dist/provenance.js +132 -0
- package/dist/recompile-marker.d.ts +19 -0
- package/dist/recompile-marker.js +36 -0
- package/dist/registry.d.ts +99 -0
- package/dist/registry.js +168 -0
- package/dist/responder.d.ts +47 -0
- package/dist/responder.js +78 -0
- package/dist/sandbox.d.ts +120 -0
- package/dist/sandbox.js +265 -0
- package/dist/self-evolution.d.ts +148 -0
- package/dist/self-evolution.js +364 -0
- package/dist/session-writer.d.ts +52 -0
- package/dist/session-writer.js +104 -0
- package/dist/sessions.d.ts +96 -0
- package/dist/sessions.js +179 -0
- package/dist/sigil.d.ts +41 -0
- package/dist/sigil.js +92 -0
- package/dist/skill-lifecycle.d.ts +74 -0
- package/dist/skill-lifecycle.js +126 -0
- package/dist/skill-review.d.ts +25 -0
- package/dist/skill-review.js +67 -0
- package/dist/spec-edit.d.ts +30 -0
- package/dist/spec-edit.js +137 -0
- package/dist/state-engine.d.ts +40 -0
- package/dist/state-engine.js +50 -0
- package/dist/state-rebuild.d.ts +48 -0
- package/dist/state-rebuild.js +60 -0
- package/dist/sync.d.ts +32 -0
- package/dist/sync.js +95 -0
- package/dist/tool-calling.d.ts +59 -0
- package/dist/tool-calling.js +168 -0
- package/dist/tool-repair.d.ts +20 -0
- package/dist/tool-repair.js +119 -0
- package/dist/tools/exec.d.ts +46 -0
- package/dist/tools/exec.js +132 -0
- package/dist/tools/registry.d.ts +38 -0
- package/dist/tools/registry.js +162 -0
- package/dist/trace.d.ts +63 -0
- package/dist/trace.js +162 -0
- package/dist/verification.d.ts +65 -0
- package/dist/verification.js +190 -0
- package/package.json +38 -0
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* F3.1 — the DETERMINISTIC faithfulness check (guards stage 2 of the pipeline).
|
|
3
|
+
*
|
|
4
|
+
* Stage 2 (the optional LLM "polish") is constrained to REPHRASE the assembled
|
|
5
|
+
* document, never to ADD or DROP claims. This check enforces that contract
|
|
6
|
+
* deterministically by diffing the polished document against the assembled one
|
|
7
|
+
* (the ground truth), section by section, over the PROTECTED claim classes:
|
|
8
|
+
*
|
|
9
|
+
* - Hard limits — a dropped safety limit is a hard failure.
|
|
10
|
+
* - Staying in character — same (these are hard limits too).
|
|
11
|
+
* - What you always/never — behavioral anchors.
|
|
12
|
+
* - What is fixed/change — consistency dimensions.
|
|
13
|
+
*
|
|
14
|
+
* The historical CMO regression — the compiled PERSONA.md invented `consistency`
|
|
15
|
+
* items the source never declared — fails here as an INVENTED finding.
|
|
16
|
+
*
|
|
17
|
+
* Matching is token-coverage based (deterministic, no model): a claim is
|
|
18
|
+
* "preserved" iff some claim on the other side shares enough content tokens.
|
|
19
|
+
* Rephrasing (synonym-free reordering, added connective words) passes; adding a
|
|
20
|
+
* genuinely new bullet or dropping one does not.
|
|
21
|
+
*/
|
|
22
|
+
const STOPWORDS = new Set([
|
|
23
|
+
"a", "an", "and", "are", "as", "at", "be", "but", "by", "for", "from", "in", "into", "is", "it",
|
|
24
|
+
"its", "of", "on", "or", "that", "the", "their", "them", "then", "they", "this", "to", "you",
|
|
25
|
+
"your", "with", "when", "what", "which", "who", "will", "not", "no", "do", "does", "done", "any",
|
|
26
|
+
"every", "each", "must", "may", "can", "cannot", "never", "always", "always:", "never:",
|
|
27
|
+
]);
|
|
28
|
+
function tokens(s) {
|
|
29
|
+
return new Set(s
|
|
30
|
+
.toLowerCase()
|
|
31
|
+
.replace(/[`*_#>[\]()"'.,;:!?]/g, " ")
|
|
32
|
+
.split(/\s+/)
|
|
33
|
+
.map((t) => t.replace(/s$/, "")) // crude singularize so plural rephrasing still matches
|
|
34
|
+
.filter((t) => t.length > 2 && !STOPWORDS.has(t)));
|
|
35
|
+
}
|
|
36
|
+
/** Content-token coverage of `a` by `b`: |a∩b| / |a|. */
|
|
37
|
+
function coverage(a, b) {
|
|
38
|
+
if (a.size === 0)
|
|
39
|
+
return 1;
|
|
40
|
+
let hit = 0;
|
|
41
|
+
for (const t of a)
|
|
42
|
+
if (b.has(t))
|
|
43
|
+
hit++;
|
|
44
|
+
return hit / a.size;
|
|
45
|
+
}
|
|
46
|
+
/** Extract the bullet claims under each protected `## section` heading. */
|
|
47
|
+
function claimsBySection(doc) {
|
|
48
|
+
const map = new Map();
|
|
49
|
+
const lines = doc.split(/\r?\n/);
|
|
50
|
+
let current;
|
|
51
|
+
for (const line of lines) {
|
|
52
|
+
const h = line.match(/^##\s+(.*)$/);
|
|
53
|
+
if (h) {
|
|
54
|
+
current = h[1].trim().toLowerCase();
|
|
55
|
+
map.set(current, []);
|
|
56
|
+
continue;
|
|
57
|
+
}
|
|
58
|
+
const b = line.match(/^\s*[-*]\s+(.*)$/);
|
|
59
|
+
if (b && current) {
|
|
60
|
+
const text = b[1].replace(/^\*\*[^*]+\*\*:?\s*/, "").trim(); // drop a leading **bold:** label
|
|
61
|
+
if (text)
|
|
62
|
+
map.get(current).push(text);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
return map;
|
|
66
|
+
}
|
|
67
|
+
const DEFAULT_SECTIONS = [
|
|
68
|
+
"hard limits (never overridden)",
|
|
69
|
+
"staying in character",
|
|
70
|
+
"what you always / never do",
|
|
71
|
+
"what is fixed, what can change",
|
|
72
|
+
];
|
|
73
|
+
/**
|
|
74
|
+
* Diff `polished` against `assembled` (the ground truth). Returns findings for
|
|
75
|
+
* dropped source claims and invented polish claims in the protected sections.
|
|
76
|
+
*/
|
|
77
|
+
export function checkFaithfulness(assembled, polished, opts = {}) {
|
|
78
|
+
const threshold = opts.threshold ?? 0.5;
|
|
79
|
+
const sections = opts.sections ?? DEFAULT_SECTIONS;
|
|
80
|
+
const src = claimsBySection(assembled);
|
|
81
|
+
const out = claimsBySection(polished);
|
|
82
|
+
const findings = [];
|
|
83
|
+
for (const section of sections) {
|
|
84
|
+
const srcClaims = (src.get(section) ?? []).map((t) => ({ text: t, tok: tokens(t) }));
|
|
85
|
+
const outClaims = (out.get(section) ?? []).map((t) => ({ text: t, tok: tokens(t) }));
|
|
86
|
+
// Dropped: a source claim with no sufficiently-covering polish claim.
|
|
87
|
+
for (const s of srcClaims) {
|
|
88
|
+
let best = 0;
|
|
89
|
+
for (const o of outClaims)
|
|
90
|
+
best = Math.max(best, coverage(s.tok, o.tok));
|
|
91
|
+
if (best < threshold)
|
|
92
|
+
findings.push({ kind: "dropped", section, text: s.text, bestCoverage: best });
|
|
93
|
+
}
|
|
94
|
+
// Invented: a polish claim with no sufficiently-covering source claim.
|
|
95
|
+
for (const o of outClaims) {
|
|
96
|
+
let best = 0;
|
|
97
|
+
for (const s of srcClaims)
|
|
98
|
+
best = Math.max(best, coverage(o.tok, s.tok));
|
|
99
|
+
if (best < threshold)
|
|
100
|
+
findings.push({ kind: "invented", section, text: o.text, bestCoverage: best });
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
return { ok: findings.length === 0, findings };
|
|
104
|
+
}
|
|
105
|
+
/** One-line human summary of a report (for CLI output / logs). */
|
|
106
|
+
export function summarizeFaithfulness(report) {
|
|
107
|
+
if (report.ok)
|
|
108
|
+
return "faithfulness: OK (polish preserved every protected claim)";
|
|
109
|
+
const dropped = report.findings.filter((f) => f.kind === "dropped").length;
|
|
110
|
+
const invented = report.findings.filter((f) => f.kind === "invented").length;
|
|
111
|
+
return `faithfulness: FAIL — ${dropped} dropped, ${invented} invented protected claim(s)`;
|
|
112
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* F3.1 — the two-stage compile pipeline, framework-agnostic and in core so the
|
|
3
|
+
* Living Loop (inline recompile) and the SaaS (server-side compile) share it
|
|
4
|
+
* with the CLI. Stage 1 (assemble) is deterministic and always runs; stage 2
|
|
5
|
+
* (LLM polish) is optional and validated by the faithfulness check.
|
|
6
|
+
*/
|
|
7
|
+
export * from "./assemble.js";
|
|
8
|
+
export * from "./faithfulness.js";
|
|
9
|
+
export * from "./targets.js";
|
|
10
|
+
export * from "./dist.js";
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* F3.1 — the two-stage compile pipeline, framework-agnostic and in core so the
|
|
3
|
+
* Living Loop (inline recompile) and the SaaS (server-side compile) share it
|
|
4
|
+
* with the CLI. Stage 1 (assemble) is deterministic and always runs; stage 2
|
|
5
|
+
* (LLM polish) is optional and validated by the faithfulness check.
|
|
6
|
+
*/
|
|
7
|
+
export * from "./assemble.js";
|
|
8
|
+
export * from "./faithfulness.js";
|
|
9
|
+
export * from "./targets.js";
|
|
10
|
+
export * from "./dist.js";
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* F3.2 — the host placement target registry (in core so the SaaS can place
|
|
3
|
+
* documents server-side, not only the CLI).
|
|
4
|
+
*
|
|
5
|
+
* A COMPILE TARGET adapts the canonical compiled document (produced by the
|
|
6
|
+
* stage-1 assembler + optional polish) into a specific host's identity/subagent
|
|
7
|
+
* convention — computing both the path and the content. Targets are registered
|
|
8
|
+
* as plugins: `registerTarget()` adds one, `getTarget()`/`listTargets()` read
|
|
9
|
+
* the registry, and the four built-ins (claude-code, codex, openclaw, hermes)
|
|
10
|
+
* are registered on import.
|
|
11
|
+
*
|
|
12
|
+
* claude-code root → <rootOutputPath> (shared) · sub → .claude/agents/<slug>.md
|
|
13
|
+
* codex root → <rootOutputPath> (shared) · sub → .codex/agents/<slug>.toml
|
|
14
|
+
* openclaw root → SOUL.md · sub → .openclaw/agents/<slug>/SOUL.md
|
|
15
|
+
* hermes root → .hermes/SOUL.md · sub → .hermes/agents/<slug>/SOUL.md
|
|
16
|
+
*
|
|
17
|
+
* SOUL.md hosts (openclaw, Hermes) read the file as the FIRST system-prompt
|
|
18
|
+
* section and RE-READ it fresh at the start of every message/session — so a
|
|
19
|
+
* recompile takes effect with no restart (hot reload). Claude Code / Codex read
|
|
20
|
+
* the root document via an `@PERSONA.md` reference from CLAUDE.md / AGENTS.md,
|
|
21
|
+
* so only a SUBAGENT compile places a new file for them.
|
|
22
|
+
*/
|
|
23
|
+
export interface PlacementContext {
|
|
24
|
+
isSubagent: boolean;
|
|
25
|
+
slug?: string;
|
|
26
|
+
/** Where the canonical root document lives (e.g. "PERSONA.md") — used by shared-root targets. */
|
|
27
|
+
rootOutputPath: string;
|
|
28
|
+
}
|
|
29
|
+
export interface PlacementResult {
|
|
30
|
+
/** Relative path the placed document should be written to. */
|
|
31
|
+
path: string;
|
|
32
|
+
content: string;
|
|
33
|
+
}
|
|
34
|
+
export interface CompileTarget {
|
|
35
|
+
/** Stable id used on the CLI (`--platform <id>`) and in configs. */
|
|
36
|
+
id: string;
|
|
37
|
+
/** True for hosts that read SOUL.md at the workspace/profile root (no @PERSONA.md baseline). */
|
|
38
|
+
isSoul: boolean;
|
|
39
|
+
/** Adapt the compiled document to this host's convention. */
|
|
40
|
+
place(compiledText: string, ctx: PlacementContext): PlacementResult;
|
|
41
|
+
}
|
|
42
|
+
/** TOML string literal (JSON encoding is a valid TOML basic string). */
|
|
43
|
+
export declare function tomlString(value: string): string;
|
|
44
|
+
/**
|
|
45
|
+
* SOUL.md placement — reuse the canonical compiled document (already a
|
|
46
|
+
* second-person qualitative identity doc), stripping only the subagent
|
|
47
|
+
* `name`/`description` frontmatter that openclaw/Hermes don't use.
|
|
48
|
+
*/
|
|
49
|
+
export declare function toSoulMd(compiledText: string): string;
|
|
50
|
+
/** Register (or override) a compile target plugin. */
|
|
51
|
+
export declare function registerTarget(target: CompileTarget): void;
|
|
52
|
+
/** Resolve a target by id (undefined when unknown). */
|
|
53
|
+
export declare function getTarget(id: string): CompileTarget | undefined;
|
|
54
|
+
/** All registered target ids, in registration order. */
|
|
55
|
+
export declare function listTargets(): string[];
|
|
56
|
+
/** The built-in host ids (stable order), for CLI help and validation. */
|
|
57
|
+
export declare const BUILTIN_TARGETS: readonly ["claude-code", "codex", "openclaw", "hermes"];
|
|
58
|
+
export type BuiltinTarget = (typeof BUILTIN_TARGETS)[number];
|
|
59
|
+
/** Convenience: place a document for a host id. Throws on an unknown target. */
|
|
60
|
+
export declare function placeForTarget(compiledText: string, targetId: string, ctx: PlacementContext): PlacementResult;
|
|
61
|
+
/** Whether a host reads SOUL.md at the root (no @PERSONA.md baseline injection). */
|
|
62
|
+
export declare function isSoulTarget(targetId: string | undefined): boolean;
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* F3.2 — the host placement target registry (in core so the SaaS can place
|
|
3
|
+
* documents server-side, not only the CLI).
|
|
4
|
+
*
|
|
5
|
+
* A COMPILE TARGET adapts the canonical compiled document (produced by the
|
|
6
|
+
* stage-1 assembler + optional polish) into a specific host's identity/subagent
|
|
7
|
+
* convention — computing both the path and the content. Targets are registered
|
|
8
|
+
* as plugins: `registerTarget()` adds one, `getTarget()`/`listTargets()` read
|
|
9
|
+
* the registry, and the four built-ins (claude-code, codex, openclaw, hermes)
|
|
10
|
+
* are registered on import.
|
|
11
|
+
*
|
|
12
|
+
* claude-code root → <rootOutputPath> (shared) · sub → .claude/agents/<slug>.md
|
|
13
|
+
* codex root → <rootOutputPath> (shared) · sub → .codex/agents/<slug>.toml
|
|
14
|
+
* openclaw root → SOUL.md · sub → .openclaw/agents/<slug>/SOUL.md
|
|
15
|
+
* hermes root → .hermes/SOUL.md · sub → .hermes/agents/<slug>/SOUL.md
|
|
16
|
+
*
|
|
17
|
+
* SOUL.md hosts (openclaw, Hermes) read the file as the FIRST system-prompt
|
|
18
|
+
* section and RE-READ it fresh at the start of every message/session — so a
|
|
19
|
+
* recompile takes effect with no restart (hot reload). Claude Code / Codex read
|
|
20
|
+
* the root document via an `@PERSONA.md` reference from CLAUDE.md / AGENTS.md,
|
|
21
|
+
* so only a SUBAGENT compile places a new file for them.
|
|
22
|
+
*/
|
|
23
|
+
import matter from "gray-matter";
|
|
24
|
+
/** TOML string literal (JSON encoding is a valid TOML basic string). */
|
|
25
|
+
export function tomlString(value) {
|
|
26
|
+
return JSON.stringify(value);
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* SOUL.md placement — reuse the canonical compiled document (already a
|
|
30
|
+
* second-person qualitative identity doc), stripping only the subagent
|
|
31
|
+
* `name`/`description` frontmatter that openclaw/Hermes don't use.
|
|
32
|
+
*/
|
|
33
|
+
export function toSoulMd(compiledText) {
|
|
34
|
+
const { content } = matter(compiledText);
|
|
35
|
+
const body = content.trim();
|
|
36
|
+
return body.startsWith("#") ? body : `# SOUL\n\n${body}`;
|
|
37
|
+
}
|
|
38
|
+
const claudeCodeTarget = {
|
|
39
|
+
id: "claude-code",
|
|
40
|
+
isSoul: false,
|
|
41
|
+
place(compiledText, ctx) {
|
|
42
|
+
if (!ctx.isSubagent)
|
|
43
|
+
return { path: ctx.rootOutputPath, content: compiledText };
|
|
44
|
+
return { path: `.claude/agents/${ctx.slug ?? "agent"}.md`, content: compiledText };
|
|
45
|
+
},
|
|
46
|
+
};
|
|
47
|
+
const codexTarget = {
|
|
48
|
+
id: "codex",
|
|
49
|
+
isSoul: false,
|
|
50
|
+
place(compiledText, ctx) {
|
|
51
|
+
if (!ctx.isSubagent)
|
|
52
|
+
return { path: ctx.rootOutputPath, content: compiledText };
|
|
53
|
+
const slug = ctx.slug ?? "agent";
|
|
54
|
+
const { data, content } = matter(compiledText);
|
|
55
|
+
const name = typeof data.name === "string" ? data.name : slug;
|
|
56
|
+
const description = typeof data.description === "string" ? data.description : "";
|
|
57
|
+
const toml = [
|
|
58
|
+
`name = ${tomlString(name)}`,
|
|
59
|
+
`description = ${tomlString(description)}`,
|
|
60
|
+
`developer_instructions = ${tomlString(content.trim())}`,
|
|
61
|
+
].join("\n") + "\n";
|
|
62
|
+
return { path: `.codex/agents/${slug}.toml`, content: toml };
|
|
63
|
+
},
|
|
64
|
+
};
|
|
65
|
+
const openclawTarget = {
|
|
66
|
+
id: "openclaw",
|
|
67
|
+
isSoul: true,
|
|
68
|
+
place(compiledText, ctx) {
|
|
69
|
+
const slug = ctx.slug ?? "agent";
|
|
70
|
+
return {
|
|
71
|
+
path: ctx.isSubagent ? `.openclaw/agents/${slug}/SOUL.md` : "SOUL.md",
|
|
72
|
+
content: toSoulMd(compiledText),
|
|
73
|
+
};
|
|
74
|
+
},
|
|
75
|
+
};
|
|
76
|
+
const hermesTarget = {
|
|
77
|
+
id: "hermes",
|
|
78
|
+
isSoul: true,
|
|
79
|
+
place(compiledText, ctx) {
|
|
80
|
+
const slug = ctx.slug ?? "agent";
|
|
81
|
+
return {
|
|
82
|
+
path: ctx.isSubagent ? `.hermes/agents/${slug}/SOUL.md` : ".hermes/SOUL.md",
|
|
83
|
+
content: toSoulMd(compiledText),
|
|
84
|
+
};
|
|
85
|
+
},
|
|
86
|
+
};
|
|
87
|
+
const registry = new Map();
|
|
88
|
+
/** Register (or override) a compile target plugin. */
|
|
89
|
+
export function registerTarget(target) {
|
|
90
|
+
registry.set(target.id, target);
|
|
91
|
+
}
|
|
92
|
+
/** Resolve a target by id (undefined when unknown). */
|
|
93
|
+
export function getTarget(id) {
|
|
94
|
+
return registry.get(id);
|
|
95
|
+
}
|
|
96
|
+
/** All registered target ids, in registration order. */
|
|
97
|
+
export function listTargets() {
|
|
98
|
+
return [...registry.keys()];
|
|
99
|
+
}
|
|
100
|
+
for (const t of [claudeCodeTarget, codexTarget, openclawTarget, hermesTarget])
|
|
101
|
+
registerTarget(t);
|
|
102
|
+
/** The built-in host ids (stable order), for CLI help and validation. */
|
|
103
|
+
export const BUILTIN_TARGETS = ["claude-code", "codex", "openclaw", "hermes"];
|
|
104
|
+
/** Convenience: place a document for a host id. Throws on an unknown target. */
|
|
105
|
+
export function placeForTarget(compiledText, targetId, ctx) {
|
|
106
|
+
const target = getTarget(targetId);
|
|
107
|
+
if (!target)
|
|
108
|
+
throw new Error(`Unknown compile target "${targetId}". Known: ${listTargets().join(", ")}`);
|
|
109
|
+
return target.place(compiledText, ctx);
|
|
110
|
+
}
|
|
111
|
+
/** Whether a host reads SOUL.md at the root (no @PERSONA.md baseline injection). */
|
|
112
|
+
export function isSoulTarget(targetId) {
|
|
113
|
+
return targetId ? getTarget(targetId)?.isSoul ?? false : false;
|
|
114
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* FR.5 — explicit numeric configuration-layer precedence.
|
|
3
|
+
*
|
|
4
|
+
* The Codex `config_layer_source` pattern (converging with Claude Code's
|
|
5
|
+
* managed→user deny-wins): every configurable value belongs to a LAYER with a
|
|
6
|
+
* numeric rank; higher rank wins, and the winner is ATTRIBUTABLE (a host can
|
|
7
|
+
* always answer "why is this value in effect?").
|
|
8
|
+
*
|
|
9
|
+
* Two tiers of keys:
|
|
10
|
+
* - ordinary keys → highest layer wins (resolveLayered);
|
|
11
|
+
* - POLICY-TIER keys → a lower layer may only RESTRICT, never relax
|
|
12
|
+
* (resolvePolicyTier, strictest-wins) — the generalization of the
|
|
13
|
+
* improvement-mode min-wins rule (SPEC.md §7.2).
|
|
14
|
+
*/
|
|
15
|
+
export declare const CONFIG_LAYERS: {
|
|
16
|
+
/** Org-managed defaults (reserved — MDM/enterprise distribution). */
|
|
17
|
+
readonly managed: 0;
|
|
18
|
+
/** ~/.personaxis/config.json */
|
|
19
|
+
readonly global: 10;
|
|
20
|
+
/** <project>/.personaxis/config.json */
|
|
21
|
+
readonly project: 20;
|
|
22
|
+
/** per-persona settings (config.json personas.<slug> section) */
|
|
23
|
+
readonly persona: 25;
|
|
24
|
+
/** the persona document's own frontmatter (runtime block) */
|
|
25
|
+
readonly frontmatter: 28;
|
|
26
|
+
/** PERSONAXIS_* environment variables / session flags */
|
|
27
|
+
readonly env: 30;
|
|
28
|
+
};
|
|
29
|
+
export type ConfigLayer = keyof typeof CONFIG_LAYERS;
|
|
30
|
+
export interface LayeredValue<T> {
|
|
31
|
+
value: T;
|
|
32
|
+
/** The layer that supplied the winning value. */
|
|
33
|
+
source: ConfigLayer;
|
|
34
|
+
}
|
|
35
|
+
/** Highest-ranked defined layer wins. Undefined when no layer defines it. */
|
|
36
|
+
export declare function resolveLayered<T>(values: Partial<Record<ConfigLayer, T | undefined>>): LayeredValue<T> | undefined;
|
|
37
|
+
/**
|
|
38
|
+
* Policy-tier resolution: the value is ordered by `strictness` (index 0 =
|
|
39
|
+
* most permissive) and the STRICTEST defined layer wins regardless of rank —
|
|
40
|
+
* a lower layer can tighten what a higher layer set, never loosen it.
|
|
41
|
+
*/
|
|
42
|
+
export declare function resolvePolicyTier<T>(values: Partial<Record<ConfigLayer, T | undefined>>, strictness: readonly T[]): LayeredValue<T> | undefined;
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* FR.5 — explicit numeric configuration-layer precedence.
|
|
3
|
+
*
|
|
4
|
+
* The Codex `config_layer_source` pattern (converging with Claude Code's
|
|
5
|
+
* managed→user deny-wins): every configurable value belongs to a LAYER with a
|
|
6
|
+
* numeric rank; higher rank wins, and the winner is ATTRIBUTABLE (a host can
|
|
7
|
+
* always answer "why is this value in effect?").
|
|
8
|
+
*
|
|
9
|
+
* Two tiers of keys:
|
|
10
|
+
* - ordinary keys → highest layer wins (resolveLayered);
|
|
11
|
+
* - POLICY-TIER keys → a lower layer may only RESTRICT, never relax
|
|
12
|
+
* (resolvePolicyTier, strictest-wins) — the generalization of the
|
|
13
|
+
* improvement-mode min-wins rule (SPEC.md §7.2).
|
|
14
|
+
*/
|
|
15
|
+
export const CONFIG_LAYERS = {
|
|
16
|
+
/** Org-managed defaults (reserved — MDM/enterprise distribution). */
|
|
17
|
+
managed: 0,
|
|
18
|
+
/** ~/.personaxis/config.json */
|
|
19
|
+
global: 10,
|
|
20
|
+
/** <project>/.personaxis/config.json */
|
|
21
|
+
project: 20,
|
|
22
|
+
/** per-persona settings (config.json personas.<slug> section) */
|
|
23
|
+
persona: 25,
|
|
24
|
+
/** the persona document's own frontmatter (runtime block) */
|
|
25
|
+
frontmatter: 28,
|
|
26
|
+
/** PERSONAXIS_* environment variables / session flags */
|
|
27
|
+
env: 30,
|
|
28
|
+
};
|
|
29
|
+
/** Highest-ranked defined layer wins. Undefined when no layer defines it. */
|
|
30
|
+
export function resolveLayered(values) {
|
|
31
|
+
let winner;
|
|
32
|
+
for (const [layer, rank] of Object.entries(CONFIG_LAYERS)) {
|
|
33
|
+
const v = values[layer];
|
|
34
|
+
if (v === undefined)
|
|
35
|
+
continue;
|
|
36
|
+
if (!winner || rank >= CONFIG_LAYERS[winner.source])
|
|
37
|
+
winner = { value: v, source: layer };
|
|
38
|
+
}
|
|
39
|
+
return winner;
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Policy-tier resolution: the value is ordered by `strictness` (index 0 =
|
|
43
|
+
* most permissive) and the STRICTEST defined layer wins regardless of rank —
|
|
44
|
+
* a lower layer can tighten what a higher layer set, never loosen it.
|
|
45
|
+
*/
|
|
46
|
+
export function resolvePolicyTier(values, strictness) {
|
|
47
|
+
let winner;
|
|
48
|
+
for (const layer of Object.keys(CONFIG_LAYERS)) {
|
|
49
|
+
const v = values[layer];
|
|
50
|
+
if (v === undefined)
|
|
51
|
+
continue;
|
|
52
|
+
const idx = strictness.indexOf(v);
|
|
53
|
+
if (idx === -1)
|
|
54
|
+
continue; // unknown value: never wins a policy decision
|
|
55
|
+
if (!winner || idx > strictness.indexOf(winner.value))
|
|
56
|
+
winner = { value: v, source: layer };
|
|
57
|
+
}
|
|
58
|
+
return winner;
|
|
59
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Agent-config security scanner (v0.9 — the "Shield" wedge).
|
|
3
|
+
*
|
|
4
|
+
* AI-agent config files (CLAUDE.md, AGENTS.md, .cursorrules, .codex/*.toml,
|
|
5
|
+
* agents.json, personaxis.md) are an unattended attack surface: prompt injection
|
|
6
|
+
* hidden in instructions/skill descriptions, over-broad permissions, and leaked
|
|
7
|
+
* credentials. (AgentShield reports 520 of 17,022 audited skills leak creds.) This
|
|
8
|
+
* scanner audits any of those, cross-harness, in three passes — red-team (find the
|
|
9
|
+
* attack), blue-team (check the boundaries), auditor (leaks + structured report) —
|
|
10
|
+
* reusing the engine's injection scanner. Pure + dependency-free; CLI exit codes.
|
|
11
|
+
*/
|
|
12
|
+
export type ConfigKind = "personaxis" | "persona-md" | "claude-md" | "agents-md" | "cursorrules" | "codex-toml" | "agents-json" | "unknown";
|
|
13
|
+
export type ScanTeam = "red" | "blue" | "auditor";
|
|
14
|
+
export type ScanSeverity = "error" | "warning" | "info";
|
|
15
|
+
export interface ScanFinding {
|
|
16
|
+
rule: string;
|
|
17
|
+
severity: ScanSeverity;
|
|
18
|
+
team: ScanTeam;
|
|
19
|
+
message: string;
|
|
20
|
+
match?: string;
|
|
21
|
+
}
|
|
22
|
+
export type ScanVerdict = "clean" | "suspicious" | "risky" | "malicious";
|
|
23
|
+
export interface ConfigScanResult {
|
|
24
|
+
kind: ConfigKind;
|
|
25
|
+
verdict: ScanVerdict;
|
|
26
|
+
score: number;
|
|
27
|
+
findings: ScanFinding[];
|
|
28
|
+
}
|
|
29
|
+
export declare function detectKind(nameOrPath: string): ConfigKind;
|
|
30
|
+
export declare function scanAgentConfig(text: string, kindHint?: ConfigKind): ConfigScanResult;
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Agent-config security scanner (v0.9 — the "Shield" wedge).
|
|
3
|
+
*
|
|
4
|
+
* AI-agent config files (CLAUDE.md, AGENTS.md, .cursorrules, .codex/*.toml,
|
|
5
|
+
* agents.json, personaxis.md) are an unattended attack surface: prompt injection
|
|
6
|
+
* hidden in instructions/skill descriptions, over-broad permissions, and leaked
|
|
7
|
+
* credentials. (AgentShield reports 520 of 17,022 audited skills leak creds.) This
|
|
8
|
+
* scanner audits any of those, cross-harness, in three passes — red-team (find the
|
|
9
|
+
* attack), blue-team (check the boundaries), auditor (leaks + structured report) —
|
|
10
|
+
* reusing the engine's injection scanner. Pure + dependency-free; CLI exit codes.
|
|
11
|
+
*/
|
|
12
|
+
import matter from "gray-matter";
|
|
13
|
+
import { scanForInjection } from "./injection.js";
|
|
14
|
+
export function detectKind(nameOrPath) {
|
|
15
|
+
const n = nameOrPath.toLowerCase().replace(/\\/g, "/");
|
|
16
|
+
if (n.endsWith("personaxis.md"))
|
|
17
|
+
return "personaxis";
|
|
18
|
+
if (n.endsWith("persona.md") || /\.claude\/agents\/.*\.md$/.test(n))
|
|
19
|
+
return "persona-md";
|
|
20
|
+
if (n.endsWith("claude.md"))
|
|
21
|
+
return "claude-md";
|
|
22
|
+
if (n.endsWith("agents.md"))
|
|
23
|
+
return "agents-md";
|
|
24
|
+
if (n.endsWith(".cursorrules") || n.endsWith("persona.mdc"))
|
|
25
|
+
return "cursorrules";
|
|
26
|
+
if (n.endsWith(".toml") && n.includes("agents"))
|
|
27
|
+
return "codex-toml";
|
|
28
|
+
if (n.endsWith("agents.json"))
|
|
29
|
+
return "agents-json";
|
|
30
|
+
return "unknown";
|
|
31
|
+
}
|
|
32
|
+
// ── Credential leak patterns (auditor) ───────────────────────────────────────
|
|
33
|
+
const SECRET_PATTERNS = [
|
|
34
|
+
{ rule: "secret:openai-key", re: /\bsk-[A-Za-z0-9]{20,}\b/g },
|
|
35
|
+
{ rule: "secret:aws-access-key", re: /\bAKIA[0-9A-Z]{16}\b/g },
|
|
36
|
+
{ rule: "secret:google-key", re: /\bAIza[0-9A-Za-z_\-]{35}\b/g },
|
|
37
|
+
{ rule: "secret:github-token", re: /\bgh[posru]_[A-Za-z0-9]{30,}\b/g },
|
|
38
|
+
{ rule: "secret:slack-token", re: /\bxox[baprs]-[A-Za-z0-9-]{10,}\b/g },
|
|
39
|
+
{ rule: "secret:bearer", re: /\bBearer\s+[A-Za-z0-9._\-]{16,}/g },
|
|
40
|
+
{ rule: "secret:private-key", re: /-----BEGIN (?:RSA |EC |OPENSSH )?PRIVATE KEY-----/g },
|
|
41
|
+
{ rule: "secret:assignment", re: /\b(?:api[_-]?key|secret|password|access[_-]?token)\b\s*[:=]\s*["']?[A-Za-z0-9_\-]{12,}/gi },
|
|
42
|
+
];
|
|
43
|
+
// ── Dangerous-permission tokens (blue-team) ──────────────────────────────────
|
|
44
|
+
const DANGER_TOKENS = [
|
|
45
|
+
{ rule: "perm:full-access", re: /danger-full-access|"?bypassPermissions"?|dangerously[-_]?skip/i, message: "grants unrestricted access (no sandbox)" },
|
|
46
|
+
{ rule: "perm:approval-never", re: /approval\s*[:=]\s*["']?never/i, message: "never asks for approval before risky actions" },
|
|
47
|
+
{ rule: "perm:autorun", re: /auto[-_]?(approve|run|accept)\s*[:=]\s*(true|all|yes)/i, message: "auto-approves/auto-runs actions" },
|
|
48
|
+
];
|
|
49
|
+
// ── Remote skill/source references (auditor — supply chain) ──────────────────
|
|
50
|
+
const REMOTE_SOURCE = /\b(github:[\w./-]+|https?:\/\/[^\s"')]+)/gi;
|
|
51
|
+
function inject(findings, f) {
|
|
52
|
+
findings.push(f);
|
|
53
|
+
}
|
|
54
|
+
export function scanAgentConfig(text, kindHint) {
|
|
55
|
+
const kind = kindHint ?? "unknown";
|
|
56
|
+
const findings = [];
|
|
57
|
+
// RED TEAM — prompt injection in the config body / instructions / descriptions.
|
|
58
|
+
const scan = scanForInjection(text);
|
|
59
|
+
if (scan.verdict !== "clean") {
|
|
60
|
+
inject(findings, {
|
|
61
|
+
rule: `injection:${scan.verdict}`,
|
|
62
|
+
severity: scan.verdict === "malicious" ? "error" : "warning",
|
|
63
|
+
team: "red",
|
|
64
|
+
message: `prompt-injection signals (${scan.findings.map((x) => x.rule).slice(0, 4).join(", ")})`,
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
// Classic override phrases (caught explicitly even when sub-threshold).
|
|
68
|
+
for (const m of text.matchAll(/ignore (?:all )?previous instructions|disregard (?:the )?(?:above|system)|reveal (?:your )?system prompt|you are now/gi)) {
|
|
69
|
+
inject(findings, { rule: "injection:override-phrase", severity: "warning", team: "red", message: "instruction-override phrase present", match: m[0].slice(0, 60) });
|
|
70
|
+
}
|
|
71
|
+
// BLUE TEAM — permission posture.
|
|
72
|
+
for (const d of DANGER_TOKENS) {
|
|
73
|
+
const m = text.match(d.re);
|
|
74
|
+
if (m)
|
|
75
|
+
inject(findings, { rule: d.rule, severity: "error", team: "blue", message: d.message, match: m[0].slice(0, 60) });
|
|
76
|
+
}
|
|
77
|
+
// For personaxis/persona configs, parse the permissions block and check guards.
|
|
78
|
+
if (kind === "personaxis" || kind === "persona-md") {
|
|
79
|
+
try {
|
|
80
|
+
const fm = matter(text).data;
|
|
81
|
+
const perms = fm.permissions;
|
|
82
|
+
if (perms) {
|
|
83
|
+
const denies = Array.isArray(perms.deny) ? perms.deny.map(String) : [];
|
|
84
|
+
const hasRmGuard = denies.some((d) => /rm\b|rmdir|del\b/.test(d));
|
|
85
|
+
const hasPipeGuard = denies.some((d) => /curl|wget|\|\s*(ba)?sh/.test(d));
|
|
86
|
+
if (perms.sandbox !== "read-only" && !hasRmGuard)
|
|
87
|
+
inject(findings, { rule: "perm:no-rm-guard", severity: "warning", team: "blue", message: "writable sandbox without a deny rule for destructive `rm`/`del`" });
|
|
88
|
+
if (perms.sandbox === "danger-full-access" && !hasPipeGuard)
|
|
89
|
+
inject(findings, { rule: "perm:no-pipe-guard", severity: "warning", team: "blue", message: "full access without a deny rule for `curl|sh` remote execution" });
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
catch {
|
|
93
|
+
/* not valid frontmatter; the token scan above still applies */
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
// AUDITOR — credential leaks.
|
|
97
|
+
for (const s of SECRET_PATTERNS) {
|
|
98
|
+
for (const m of text.matchAll(s.re)) {
|
|
99
|
+
inject(findings, { rule: s.rule, severity: "error", team: "auditor", message: "looks like a hardcoded credential — never ship secrets in a config", match: m[0].slice(0, 8) + "…" });
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
// AUDITOR — remote skill/source references to review (supply chain).
|
|
103
|
+
const remotes = new Set();
|
|
104
|
+
for (const m of text.matchAll(REMOTE_SOURCE))
|
|
105
|
+
remotes.add(m[1]);
|
|
106
|
+
for (const r of [...remotes].slice(0, 20)) {
|
|
107
|
+
if (/skill|agent|prompt|tool/i.test(r) || r.startsWith("github:"))
|
|
108
|
+
inject(findings, { rule: "supply-chain:remote-source", severity: "info", team: "auditor", message: "external source — audit before trusting", match: r.slice(0, 60) });
|
|
109
|
+
}
|
|
110
|
+
// Score + verdict.
|
|
111
|
+
const weight = (s) => (s === "error" ? 1 : s === "warning" ? 0.4 : 0.1);
|
|
112
|
+
const score = Number(findings.reduce((a, f) => a + weight(f.severity), 0).toFixed(2));
|
|
113
|
+
const hasLeakOrMalicious = findings.some((f) => f.team === "auditor" && f.severity === "error") || findings.some((f) => f.rule === "injection:malicious");
|
|
114
|
+
const hasError = findings.some((f) => f.severity === "error");
|
|
115
|
+
const hasWarn = findings.some((f) => f.severity === "warning");
|
|
116
|
+
const verdict = hasLeakOrMalicious ? "malicious" : hasError ? "risky" : hasWarn ? "suspicious" : "clean";
|
|
117
|
+
return { kind, verdict, score, findings };
|
|
118
|
+
}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Context-window management (the bottom-bar meter + auto-compaction).
|
|
3
|
+
*
|
|
4
|
+
* Every serious agent tracks how full the model's context window is and compacts
|
|
5
|
+
* before it overflows (Claude Code auto-compacts ~80%; Hermes shows `model │
|
|
6
|
+
* 14.4K/256K │ %`). Two hard parts handled here, model-agnostically:
|
|
7
|
+
* 1. The window VARIES per model and we won't hardcode thousands — so we resolve
|
|
8
|
+
* it dynamically from the endpoint's `/models` (OpenRouter/Ollama expose
|
|
9
|
+
* `context_length`), cache it, and fall back to a small pattern table.
|
|
10
|
+
* 2. Compaction must fire with HEADROOM (default 0.8): summarizing is itself a
|
|
11
|
+
* model call that needs room for the conversation + the summary; waiting for
|
|
12
|
+
* 100% leaves no space and most providers hard-error at the limit.
|
|
13
|
+
*/
|
|
14
|
+
import type { TokenUsage } from "./tool-calling.js";
|
|
15
|
+
import type { ChatMessage } from "./tool-calling.js";
|
|
16
|
+
export interface ModelEndpoint {
|
|
17
|
+
endpoint: string;
|
|
18
|
+
model: string;
|
|
19
|
+
apiKey?: string;
|
|
20
|
+
fetchImpl?: typeof fetch;
|
|
21
|
+
}
|
|
22
|
+
export declare function tableContextWindow(model: string): number;
|
|
23
|
+
/** Synchronous best-known window (cache → table). Safe for render paths. */
|
|
24
|
+
export declare function cachedContextWindow(model: string): number;
|
|
25
|
+
/**
|
|
26
|
+
* Resolve the model's context window, best-effort: query `{endpoint}/models`,
|
|
27
|
+
* read context_length/context_window, cache it; else fall back to the table.
|
|
28
|
+
* Never throws; never blocks startup beyond a short timeout.
|
|
29
|
+
*/
|
|
30
|
+
export declare function resolveContextWindow(cfg: ModelEndpoint, timeoutMs?: number): Promise<number>;
|
|
31
|
+
/** Rough token estimate when the provider doesn't report usage (~4 chars/token). */
|
|
32
|
+
export declare function estimateTokens(text: string): number;
|
|
33
|
+
export declare function estimateMessagesTokens(messages: ChatMessage[]): number;
|
|
34
|
+
/**
|
|
35
|
+
* Tracks how full the context window is across a session. `used` is the size of
|
|
36
|
+
* the last prompt sent (the live context), preferring provider-reported tokens.
|
|
37
|
+
*/
|
|
38
|
+
export declare class ContextMeter {
|
|
39
|
+
limit: number;
|
|
40
|
+
used: number;
|
|
41
|
+
readonly startedAt: number;
|
|
42
|
+
constructor(limit: number);
|
|
43
|
+
/** Record the provider's reported usage for the last call. */
|
|
44
|
+
observe(usage?: TokenUsage): void;
|
|
45
|
+
/** Fallback: estimate from the current message array. */
|
|
46
|
+
estimate(messages: ChatMessage[]): void;
|
|
47
|
+
get pct(): number;
|
|
48
|
+
get elapsedSeconds(): number;
|
|
49
|
+
}
|
|
50
|
+
export interface CompactOptions {
|
|
51
|
+
llm: ModelEndpoint;
|
|
52
|
+
threshold?: number;
|
|
53
|
+
keepLastN?: number;
|
|
54
|
+
}
|
|
55
|
+
export interface CompactResult {
|
|
56
|
+
messages: ChatMessage[];
|
|
57
|
+
compacted: boolean;
|
|
58
|
+
summary?: string;
|
|
59
|
+
removed?: number;
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* Compact the conversation when the meter crosses the threshold: summarize the
|
|
63
|
+
* older messages into one, keep the system message + the last N turns. The
|
|
64
|
+
* summary is produced by the model itself (Claude-Code style). Best-effort: if the
|
|
65
|
+
* summarizer call fails, returns the messages unchanged (never breaks the session).
|
|
66
|
+
*/
|
|
67
|
+
export declare function compactMessages(messages: ChatMessage[], meter: ContextMeter, opts: CompactOptions): Promise<CompactResult>;
|