litcodex-ai 0.3.10 → 0.3.12
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/node_modules/@litcodex/lit-loop/dist/cli.js +7 -6
- package/node_modules/@litcodex/lit-loop/dist/codex-goal-instruction.js +1 -0
- package/node_modules/@litcodex/lit-loop/dist/codex-hook.d.ts +13 -0
- package/node_modules/@litcodex/lit-loop/dist/codex-hook.js +36 -0
- package/node_modules/@litcodex/lit-loop/dist/hook-cli.d.ts +1 -0
- package/node_modules/@litcodex/lit-loop/dist/hook-cli.js +28 -1
- package/node_modules/@litcodex/lit-loop/package.json +1 -1
- package/package.json +2 -2
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
//
|
|
15
15
|
// `main` NEVER calls process.exit (the shebang wrapper does); it resolves exactly one integer, so
|
|
16
16
|
// the dispatcher is unit-testable with an injected argv array.
|
|
17
|
-
import { runUserPromptSubmitHookCli } from "./hook-cli.js";
|
|
17
|
+
import { runPreToolUseCreateGoalGuardCli, runUserPromptSubmitHookCli } from "./hook-cli.js";
|
|
18
18
|
import { loopCommand } from "./loop-cli.js";
|
|
19
19
|
const UNKNOWN_COMMAND = "lit-loop: unknown command\n";
|
|
20
20
|
/**
|
|
@@ -25,11 +25,12 @@ export async function main(argv, stdin, stdout, stderr) {
|
|
|
25
25
|
const command = argv[2];
|
|
26
26
|
const subcommand = argv[3];
|
|
27
27
|
if (command === "hook") {
|
|
28
|
-
if (subcommand
|
|
29
|
-
stderr
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
28
|
+
if (subcommand === "user-prompt-submit")
|
|
29
|
+
return runUserPromptSubmitHookCli(stdin, stdout, stderr);
|
|
30
|
+
if (subcommand === "pre-tool-use")
|
|
31
|
+
return runPreToolUseCreateGoalGuardCli(stdin, stdout, stderr);
|
|
32
|
+
stderr.write(UNKNOWN_COMMAND);
|
|
33
|
+
return 1;
|
|
33
34
|
}
|
|
34
35
|
if (command === "loop") {
|
|
35
36
|
// T14: route to M09's loopCommand (argv past `loop`); it resolves one exit code (0/1/2/3/4/5)
|
|
@@ -25,6 +25,7 @@ function buildText(plan, goal, payload, isFinal) {
|
|
|
25
25
|
"",
|
|
26
26
|
"Codex goal integration constraints:",
|
|
27
27
|
"- Use the create_goal payload exactly as rendered: objective only.",
|
|
28
|
+
"- LitCodex also installs a PreToolUse guard that denies create_goal calls carrying token_budget, status, metadata, or any non-objective key.",
|
|
28
29
|
"- Goals are unlimited. Do not add numeric limits.",
|
|
29
30
|
"- If get_goal/create_goal/update_goal are not exposed by this Codex session, continue with .litcodex/lit-loop durable state as the source of truth and record `native-goal-unavailable` in your handoff or ledger evidence; do not claim native /goal sync.",
|
|
30
31
|
...nativeGoalModeLines(mode),
|
|
@@ -8,6 +8,11 @@ export interface LitUserPromptSubmitInput {
|
|
|
8
8
|
readonly prompt: string;
|
|
9
9
|
readonly transcript_path?: string | null;
|
|
10
10
|
}
|
|
11
|
+
export interface LitPreToolUseCreateGoalInput {
|
|
12
|
+
readonly hook_event_name: "PreToolUse";
|
|
13
|
+
readonly tool_name: "create_goal";
|
|
14
|
+
readonly tool_input: unknown;
|
|
15
|
+
}
|
|
11
16
|
/** Structured engine result. The CLI maps this to stdout / exit. */
|
|
12
17
|
export type HookDecision = {
|
|
13
18
|
readonly kind: "inject";
|
|
@@ -15,6 +20,12 @@ export type HookDecision = {
|
|
|
15
20
|
} | {
|
|
16
21
|
readonly kind: "noop";
|
|
17
22
|
};
|
|
23
|
+
export type PreToolUseCreateGoalDecision = {
|
|
24
|
+
readonly kind: "deny";
|
|
25
|
+
readonly stdout: string;
|
|
26
|
+
} | {
|
|
27
|
+
readonly kind: "noop";
|
|
28
|
+
};
|
|
18
29
|
/**
|
|
19
30
|
* Type-guard. True iff `value` is a record whose accepted event-name key (snake `hook_event_name`
|
|
20
31
|
* primary, camel `hookEventName` fallback) === "UserPromptSubmit", `prompt` is a string, and the
|
|
@@ -22,12 +33,14 @@ export type HookDecision = {
|
|
|
22
33
|
* accept-set, not a structural type assert, so a camelCase-keyed record also returns true.
|
|
23
34
|
*/
|
|
24
35
|
export declare function isLitUserPromptSubmitInput(value: unknown): value is LitUserPromptSubmitInput;
|
|
36
|
+
export declare function isLitPreToolUseCreateGoalInput(value: unknown): value is LitPreToolUseCreateGoalInput;
|
|
25
37
|
/**
|
|
26
38
|
* Wraps directive text in the Codex output envelope. Returns "" if the normalized context is empty
|
|
27
39
|
* (caller treats "" as noop). Output is a single-line JSON + trailing "\n":
|
|
28
40
|
* {"hookSpecificOutput":{"hookEventName":"UserPromptSubmit","additionalContext":<ctx>}}\n
|
|
29
41
|
*/
|
|
30
42
|
export declare function formatAdditionalContextOutput(additionalContext: string): string;
|
|
43
|
+
export declare function applyPreToolUseCreateGoalGuard(input: unknown): PreToolUseCreateGoalDecision;
|
|
31
44
|
/**
|
|
32
45
|
* Pure decision function (multi-mode router). Takes an already-parsed value (NOT a raw string).
|
|
33
46
|
* Total over `unknown`; NEVER throws. Flow: validate shape → match the lit-family trigger token
|
|
@@ -22,6 +22,9 @@ import { shouldSuppressInjection } from "./guards.js";
|
|
|
22
22
|
import { modeForToken } from "./modes.js";
|
|
23
23
|
import { matchLitTrigger } from "./trigger.js";
|
|
24
24
|
const HOOK_EVENT_NAME = "UserPromptSubmit";
|
|
25
|
+
const PRE_TOOL_USE_EVENT_NAME = "PreToolUse";
|
|
26
|
+
const CREATE_GOAL_TOOL_NAME = "create_goal";
|
|
27
|
+
const CREATE_GOAL_DENY_REASON = "Use create_goal with objective only. Omit token_budget, status, metadata, and all other keys so the Codex goal stays unlimited; record native-goal-unavailable if the host goal tools are not available.";
|
|
25
28
|
/** Narrow to a plain (non-null, non-array) record. */
|
|
26
29
|
function isRecord(value) {
|
|
27
30
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
@@ -64,6 +67,12 @@ export function isLitUserPromptSubmitInput(value) {
|
|
|
64
67
|
}
|
|
65
68
|
return isTranscriptPathValue(readTranscriptPath(value));
|
|
66
69
|
}
|
|
70
|
+
export function isLitPreToolUseCreateGoalInput(value) {
|
|
71
|
+
return (isRecord(value) &&
|
|
72
|
+
readEventName(value) === PRE_TOOL_USE_EVENT_NAME &&
|
|
73
|
+
value["tool_name"] === CREATE_GOAL_TOOL_NAME &&
|
|
74
|
+
Object.hasOwn(value, "tool_input"));
|
|
75
|
+
}
|
|
67
76
|
/**
|
|
68
77
|
* Wraps directive text in the Codex output envelope. Returns "" if the normalized context is empty
|
|
69
78
|
* (caller treats "" as noop). Output is a single-line JSON + trailing "\n":
|
|
@@ -82,6 +91,33 @@ export function formatAdditionalContextOutput(additionalContext) {
|
|
|
82
91
|
};
|
|
83
92
|
return `${JSON.stringify(output)}\n`;
|
|
84
93
|
}
|
|
94
|
+
function formatPreToolUseDenyOutput(reason) {
|
|
95
|
+
const output = {
|
|
96
|
+
hookSpecificOutput: {
|
|
97
|
+
hookEventName: PRE_TOOL_USE_EVENT_NAME,
|
|
98
|
+
permissionDecision: "deny",
|
|
99
|
+
permissionDecisionReason: reason,
|
|
100
|
+
additionalContext: reason,
|
|
101
|
+
},
|
|
102
|
+
};
|
|
103
|
+
return `${JSON.stringify(output)}\n`;
|
|
104
|
+
}
|
|
105
|
+
function hasObjectiveOnlyCreateGoalInput(value) {
|
|
106
|
+
if (!isRecord(value)) {
|
|
107
|
+
return false;
|
|
108
|
+
}
|
|
109
|
+
const keys = Object.keys(value);
|
|
110
|
+
return keys.length === 1 && keys[0] === "objective" && typeof value["objective"] === "string";
|
|
111
|
+
}
|
|
112
|
+
export function applyPreToolUseCreateGoalGuard(input) {
|
|
113
|
+
if (!isLitPreToolUseCreateGoalInput(input)) {
|
|
114
|
+
return { kind: "noop" };
|
|
115
|
+
}
|
|
116
|
+
if (hasObjectiveOnlyCreateGoalInput(input.tool_input)) {
|
|
117
|
+
return { kind: "noop" };
|
|
118
|
+
}
|
|
119
|
+
return { kind: "deny", stdout: formatPreToolUseDenyOutput(CREATE_GOAL_DENY_REASON) };
|
|
120
|
+
}
|
|
85
121
|
/**
|
|
86
122
|
* Load the matched mode's directive behind a fail-silent boundary: any loader error (e.g. a missing
|
|
87
123
|
* directives/<mode>.md or a marker-wrapper mismatch) resolves to "" so the hook degrades to a silent
|
|
@@ -15,3 +15,4 @@ export type LitHookErrorCode = "LIT_HOOK_STDIN_INVALID_JSON" | "LIT_HOOK_STDIN_T
|
|
|
15
15
|
* line to stderr on malformed / oversized stdin.
|
|
16
16
|
*/
|
|
17
17
|
export declare function runUserPromptSubmitHookCli(stdin: NodeJS.ReadableStream, stdout: NodeJS.WritableStream, stderr: NodeJS.WritableStream): Promise<number>;
|
|
18
|
+
export declare function runPreToolUseCreateGoalGuardCli(stdin: NodeJS.ReadableStream, stdout: NodeJS.WritableStream, stderr: NodeJS.WritableStream): Promise<number>;
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
// valid event → 0 (no-op writes zero bytes); unparseable or oversized stdin → 2 with a
|
|
7
7
|
// machine-readable LitHookError JSON line on stderr. NEVER calls process.exit (the dispatcher does)
|
|
8
8
|
// and NEVER throws.
|
|
9
|
-
import { runUserPromptSubmitHook } from "./codex-hook.js";
|
|
9
|
+
import { applyPreToolUseCreateGoalGuard, runUserPromptSubmitHook } from "./codex-hook.js";
|
|
10
10
|
/** Hard cap on stdin to bound memory / ReDoS exposure inside the 5 s Codex hook budget. 8 MB. */
|
|
11
11
|
export const MAX_STDIN_BYTES = 8_000_000;
|
|
12
12
|
const INVALID_JSON_MESSAGE = "lit hook: stdin was not valid JSON";
|
|
@@ -92,3 +92,30 @@ export async function runUserPromptSubmitHookCli(stdin, stdout, stderr) {
|
|
|
92
92
|
}
|
|
93
93
|
return 0;
|
|
94
94
|
}
|
|
95
|
+
export async function runPreToolUseCreateGoalGuardCli(stdin, stdout, stderr) {
|
|
96
|
+
const decoded = await readStdin(stdin);
|
|
97
|
+
if (decoded === null) {
|
|
98
|
+
stderr.write(errorLine("LIT_HOOK_STDIN_TOO_LARGE", TOO_LARGE_MESSAGE));
|
|
99
|
+
return 2;
|
|
100
|
+
}
|
|
101
|
+
let raw = decoded;
|
|
102
|
+
if (raw.charCodeAt(0) === 0xfeff) {
|
|
103
|
+
raw = raw.slice(1);
|
|
104
|
+
}
|
|
105
|
+
if (raw.trim().length === 0) {
|
|
106
|
+
return 0;
|
|
107
|
+
}
|
|
108
|
+
let parsed;
|
|
109
|
+
try {
|
|
110
|
+
parsed = JSON.parse(raw);
|
|
111
|
+
}
|
|
112
|
+
catch {
|
|
113
|
+
stderr.write(errorLine("LIT_HOOK_STDIN_INVALID_JSON", INVALID_JSON_MESSAGE));
|
|
114
|
+
return 2;
|
|
115
|
+
}
|
|
116
|
+
const decision = applyPreToolUseCreateGoalGuard(parsed);
|
|
117
|
+
if (decision.kind === "deny") {
|
|
118
|
+
stdout.write(decision.stdout);
|
|
119
|
+
}
|
|
120
|
+
return 0;
|
|
121
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "litcodex-ai",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.12",
|
|
4
4
|
"description": "Codex loop harness installer. Run `npx litcodex-ai install` to set up the LitCodex Codex platform: the bare `lit` hook and the durable lit-loop runtime.",
|
|
5
5
|
"keywords": ["codex", "litcodex", "lit-loop", "ai-agents", "orchestration"],
|
|
6
6
|
"author": "LitCodex Authors",
|
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
},
|
|
16
16
|
"files": ["bin", "dist", "model-catalog.json", "README.md", "LICENSE"],
|
|
17
17
|
"dependencies": {
|
|
18
|
-
"@litcodex/lit-loop": "0.3.
|
|
18
|
+
"@litcodex/lit-loop": "0.3.12"
|
|
19
19
|
},
|
|
20
20
|
"bundledDependencies": ["@litcodex/lit-loop"],
|
|
21
21
|
"scripts": {
|