@kl-c/matrixos 0.1.40 → 0.2.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.
@@ -0,0 +1,44 @@
1
+ /**
2
+ * Gateway message handler.
3
+ *
4
+ * Receives a normalized envelope from the MatrixGateway, dispatches to a
5
+ * MaTrixOS CLI subprocess (default: `matrixos run <text>`), and returns
6
+ * the agent reply to be sent back through the adapter.
7
+ *
8
+ * Why subprocess and not in-process: the agent runtime is OpenCode-driven
9
+ * and uses its own conversation loop. Forcing it into the gateway's event
10
+ * loop is an L-scale refactor; subprocess keeps the gateway stateless and
11
+ * the agent isolated. Each message = fresh agent process, no session
12
+ * reuse for v0 (Phase 1 limitation, documented in TRACABILITE).
13
+ */
14
+ import type { UnifiedEnvelope } from "@matrixos/matrix-gateway-core";
15
+ export interface GatewayHandlerOptions {
16
+ /** Command to invoke. Default: matrixos (resolved via PATH). */
17
+ readonly command?: string;
18
+ /** Args to pass (default: ["run", text]). Use {text} as placeholder. */
19
+ readonly args?: string[];
20
+ /** Working dir for the subprocess. */
21
+ readonly cwd?: string;
22
+ /** Timeout in ms (default 30s). */
23
+ readonly timeoutMs?: number;
24
+ /** Env overrides (merged on top of process.env, secrets redacted). */
25
+ readonly env?: Record<string, string>;
26
+ /** Optional callback for partial output (Phase 2: streaming). */
27
+ readonly onPartial?: (chunk: string) => void;
28
+ }
29
+ export interface GatewayHandlerResult {
30
+ readonly ok: boolean;
31
+ readonly reply: string;
32
+ readonly durationMs: number;
33
+ readonly exitCode: number | null;
34
+ }
35
+ /**
36
+ * Slash command detection. If the inbound text starts with /adopt, the
37
+ * handler dispatches to the adoption flow instead of the agent subprocess.
38
+ * Returns true if the command was handled inline (caller should not run
39
+ * the subprocess).
40
+ */
41
+ export declare const ADOPT_COMMAND = "/adopt";
42
+ export declare function isAdoptCommand(env: UnifiedEnvelope): boolean;
43
+ export declare function handleAdoptCommand(env: UnifiedEnvelope, opts?: GatewayHandlerOptions): Promise<GatewayHandlerResult>;
44
+ export declare function handleGatewayMessage(env: UnifiedEnvelope, opts?: GatewayHandlerOptions): Promise<GatewayHandlerResult>;
package/dist/index.js CHANGED
@@ -368023,7 +368023,7 @@ function getCachedVersion(options = {}) {
368023
368023
  // package.json
368024
368024
  var package_default = {
368025
368025
  name: "@kl-c/matrixos",
368026
- version: "0.1.40",
368026
+ version: "0.2.0",
368027
368027
  description: "MaTrixOS \u2014 Agentic OS for OpenCode. Personalizable, communicating, self-improving, resilient.",
368028
368028
  main: "./dist/index.js",
368029
368029
  types: "dist/index.d.ts",
@@ -441224,6 +441224,149 @@ async function resolveSessionAgent(client5, sessionId) {
441224
441224
  // packages/omo-opencode/src/plugin/tool-execute-before.ts
441225
441225
  init_constants3();
441226
441226
  init_storage();
441227
+
441228
+ // packages/omo-opencode/src/security/validation-guard.ts
441229
+ var PATTERNS = [
441230
+ {
441231
+ id: "bash.rm-recursive",
441232
+ description: "Recursively deletes files (rm -rf). Confirm before running.",
441233
+ match: (c) => c.tool.toLowerCase() === "bash" && typeof c.args.command === "string" && /\brm\s+(-[a-zA-Z]*r[a-zA-Z]*f|-[a-zA-Z]*f[a-zA-Z]*r)\b/.test(c.args.command)
441234
+ },
441235
+ {
441236
+ id: "bash.git-force-push",
441237
+ description: "Force-pushes git history. Can destroy remote work.",
441238
+ match: (c) => c.tool.toLowerCase() === "bash" && typeof c.args.command === "string" && /\bgit\s+push\s+(-[a-zA-Z]*f|--force(?!-with-lease))/.test(c.args.command)
441239
+ },
441240
+ {
441241
+ id: "bash.git-reset-hard",
441242
+ description: "git reset --hard destroys uncommitted changes.",
441243
+ match: (c) => c.tool.toLowerCase() === "bash" && typeof c.args.command === "string" && /\bgit\s+reset\s+--hard\b/.test(c.args.command)
441244
+ },
441245
+ {
441246
+ id: "bash.npm-publish",
441247
+ description: "Publishes a package to a public registry. Confirm version + scope.",
441248
+ match: (c) => c.tool.toLowerCase() === "bash" && typeof c.args.command === "string" && /\bnpm\s+publish\b/.test(c.args.command)
441249
+ },
441250
+ {
441251
+ id: "bash.chmod-777",
441252
+ description: "chmod 777 opens the file to everyone. Confirm intent.",
441253
+ match: (c) => c.tool.toLowerCase() === "bash" && typeof c.args.command === "string" && /\bchmod\s+(-[a-zA-Z]*R\s+)?777\b/.test(c.args.command)
441254
+ },
441255
+ {
441256
+ id: "bash.curl-pipe-bash",
441257
+ description: "Pipes a remote script into bash (remote code execution).",
441258
+ match: (c) => c.tool.toLowerCase() === "bash" && typeof c.args.command === "string" && /curl[^|]*\|\s*(sudo\s+)?bash/.test(c.args.command)
441259
+ },
441260
+ {
441261
+ id: "write.env-or-secrets",
441262
+ description: "Writes to a .env / credentials / secrets file.",
441263
+ match: (c) => {
441264
+ if (c.tool.toLowerCase() !== "write" && c.tool.toLowerCase() !== "edit")
441265
+ return false;
441266
+ const path27 = typeof c.args.filePath === "string" ? c.args.filePath : typeof c.args.path === "string" ? c.args.path : "";
441267
+ return /\.env(\.|$)|\/\.klc-secrets\/|\/\.aws\/credentials|id_rsa|id_ed25519/.test(path27);
441268
+ }
441269
+ },
441270
+ {
441271
+ id: "write.system-path",
441272
+ description: "Writes to a system path (/etc/, /usr/, /var/). Confirm intent.",
441273
+ match: (c) => {
441274
+ if (c.tool.toLowerCase() !== "write" && c.tool.toLowerCase() !== "edit")
441275
+ return false;
441276
+ const path27 = typeof c.args.filePath === "string" ? c.args.filePath : typeof c.args.path === "string" ? c.args.path : "";
441277
+ return /^\s*(\/etc\/|\/usr\/|\/var\/)/.test(path27);
441278
+ }
441279
+ }
441280
+ ];
441281
+ function checkValidation(tool3, args) {
441282
+ const call = { tool: tool3, args };
441283
+ for (const p of PATTERNS) {
441284
+ if (p.match(call)) {
441285
+ return {
441286
+ verdict: "deny",
441287
+ reason: `[P-001 Prepare-Never-Submit] Blocked: ${p.description} (pattern: ${p.id}). Ask the user to confirm before running this command.`,
441288
+ pattern: p.id
441289
+ };
441290
+ }
441291
+ }
441292
+ return { verdict: "allow" };
441293
+ }
441294
+
441295
+ // packages/omo-opencode/src/security/injection-guard.ts
441296
+ var INJECTION_PATTERNS = [
441297
+ { id: "ignore-previous", re: /\bignore\s+(all\s+)?(previous|prior|above)\s+(instructions?|directives?|prompts?)/i, description: "ignore previous instructions" },
441298
+ { id: "you-are-now", re: /\byou\s+are\s+now\s+(a|an)\s+/i, description: "you are now a \u2026" },
441299
+ { id: "system-prompt-tag", re: /<\|im_start\|>\s*system|<\|im_end\|>/i, description: "ChatML system tag (model-tag injection)" },
441300
+ { id: "assistant-tag-injection", re: /<\|im_start\|>\s*assistant\b/i, description: "ChatML assistant tag in user content" },
441301
+ { id: "developer-mode", re: /\b(developer|dan|jailbreak|do\s+anything\s+now)\s+mode\b/i, description: "jailbreak mode trigger" },
441302
+ { id: "disregard-safety", re: /\b(disregard|bypass|ignore)\s+(safety|guardrails?|restrictions?|policies)/i, description: "disregard safety guardrails" },
441303
+ { id: "execute-following", re: /\bexecute\s+the\s+following\s+(command|code|instructions?)/i, description: "execute the following \u2026" },
441304
+ { id: "new-instructions", re: /\bnew\s+instructions?:/i, description: "new instructions: header" },
441305
+ { id: "repeat-after-me", re: /\brepeat\s+after\s+me\b/i, description: "repeat after me" },
441306
+ { id: "system-colon-override", re: /^\s*system\s*:\s*[\w]/im, description: "system: override (line-start)" }
441307
+ ];
441308
+ function containsInjection(text) {
441309
+ for (const p of INJECTION_PATTERNS) {
441310
+ const m = p.re.exec(text);
441311
+ if (m) {
441312
+ const start = Math.max(0, m.index - 20);
441313
+ const end = Math.min(text.length, m.index + m[0].length + 20);
441314
+ return { id: p.id, description: p.description, snippet: text.slice(start, end) };
441315
+ }
441316
+ }
441317
+ return null;
441318
+ }
441319
+ var STRICT_FIELDS = {
441320
+ bash: {
441321
+ command: "string-narrow",
441322
+ timeout: "number"
441323
+ },
441324
+ write: {
441325
+ filePath: "string-strict",
441326
+ content: "string-narrow"
441327
+ },
441328
+ edit: {
441329
+ filePath: "string-strict",
441330
+ oldString: "string-strict",
441331
+ newString: "string-narrow"
441332
+ },
441333
+ read: {
441334
+ filePath: "string-strict"
441335
+ },
441336
+ glob: {
441337
+ pattern: "string-strict"
441338
+ },
441339
+ grep: {
441340
+ pattern: "string-strict",
441341
+ path: "string-strict"
441342
+ }
441343
+ };
441344
+ var PATH_RE = /^[^\x00-\x1f]*$/;
441345
+ var MAX_NARROW = 1e5;
441346
+ function sanitizeToolArgs(tool3, args) {
441347
+ const schema2 = STRICT_FIELDS[tool3.toLowerCase()];
441348
+ if (!schema2)
441349
+ return { ok: true };
441350
+ for (const [field, kind] of Object.entries(schema2)) {
441351
+ const v = args[field];
441352
+ if (v === undefined || v === null)
441353
+ continue;
441354
+ if (typeof v !== "string" && typeof v !== "number" && typeof v !== "boolean") {
441355
+ return { ok: false, reason: `field ${field} has wrong type ${typeof v}`, field };
441356
+ }
441357
+ if (typeof v === "string") {
441358
+ if (kind === "string-strict" && !PATH_RE.test(v)) {
441359
+ return { ok: false, reason: `field ${field} contains control characters or is empty`, field };
441360
+ }
441361
+ if (kind === "string-narrow" && v.length > MAX_NARROW) {
441362
+ return { ok: false, reason: `field ${field} too long (${v.length} > ${MAX_NARROW})`, field };
441363
+ }
441364
+ }
441365
+ }
441366
+ return { ok: true };
441367
+ }
441368
+
441369
+ // packages/omo-opencode/src/plugin/tool-execute-before.ts
441227
441370
  var BACKGROUND_WAIT_BLOCK_MESSAGE = [
441228
441371
  "Background task wait is already managed by the plugin.",
441229
441372
  "End this response now and wait for the <system-reminder> completion notification.",
@@ -441300,6 +441443,27 @@ function createToolExecuteBeforeHandler3(args) {
441300
441443
  if (egressDecision?.verdict === "deny") {
441301
441444
  throw new Error(`Egress blocked: ${egressDecision.reason}`);
441302
441445
  }
441446
+ const validation = checkValidation(input.tool, output.args);
441447
+ if (validation.verdict === "deny") {
441448
+ throw new Error(validation.reason);
441449
+ }
441450
+ const sanitize = sanitizeToolArgs(input.tool, output.args);
441451
+ if (!sanitize.ok) {
441452
+ throw new Error(`[A12 injection-guard] Tool arg rejected: ${sanitize.reason} (field: ${sanitize.field})`);
441453
+ }
441454
+ if (input.tool.toLowerCase() === "bash" && typeof output.args.command === "string") {
441455
+ const inj = containsInjection(output.args.command);
441456
+ if (inj) {
441457
+ log2("[A12 injection-guard] Possible prompt injection in bash command", {
441458
+ sessionID: input.sessionID,
441459
+ callID: input.callID,
441460
+ match: inj
441461
+ });
441462
+ if (output.args.command.includes("MTRX-CANARY-")) {
441463
+ throw new Error(`[A12 injection-guard] Bash command contains canary token \u2014 possible exfiltration attempt. Aborting.`);
441464
+ }
441465
+ }
441466
+ }
441303
441467
  await hooks.writeExistingFileGuard?.["tool.execute.before"]?.(input, output);
441304
441468
  await hooks.notepadWriteGuard?.["tool.execute.before"]?.(input, output);
441305
441469
  await hooks.questionLabelTruncator?.["tool.execute.before"]?.(input, output);
@@ -0,0 +1,34 @@
1
+ /**
2
+ * Task Progress Streaming (ANNEXE A17).
3
+ *
4
+ * Emits structured progress events for long-running tool calls so the
5
+ * gateway (or CLI) can display real-time status instead of a silent wait.
6
+ *
7
+ * NOTE: today the consumer is the structured log. The Telegram gateway
8
+ * (CDC §7) is not implemented yet — when it is, it can subscribe to
9
+ * these events to edit messages in real time. The schema is stable.
10
+ */
11
+ export type ProgressEvent = {
12
+ type: "task.progress";
13
+ taskId: string;
14
+ tool: string;
15
+ sessionID: string;
16
+ callID: string;
17
+ step: number;
18
+ totalSteps: number;
19
+ message: string;
20
+ startedAt: number;
21
+ elapsedMs: number;
22
+ };
23
+ type Listener = (e: ProgressEvent) => void;
24
+ /** Subscribe to progress events. Returns an unsubscribe function. */
25
+ export declare function onProgress(listener: Listener): () => void;
26
+ /** Start a task. Returns the taskId. */
27
+ export declare function startTask(tool: string, sessionID: string, callID: string, totalSteps?: number): string;
28
+ /** Advance the current task. */
29
+ export declare function advance(message: string): void;
30
+ /** End the current task. */
31
+ export declare function endTask(message?: string): void;
32
+ /** Run an async operation with task progress tracking. */
33
+ export declare function withProgress<T>(tool: string, sessionID: string, callID: string, op: (advance: (msg: string) => void) => Promise<T>): Promise<T>;
34
+ export {};
@@ -0,0 +1,42 @@
1
+ /**
2
+ * Mid-Task Checkpoint (ANNEXE A9).
3
+ *
4
+ * Saves the in-flight task state every 30s so a crash (network, OOM, SIGKILL)
5
+ * can resume mid-task instead of restarting from scratch.
6
+ *
7
+ * Difference from snapshot (CDC §18.7): checkpoint is automatic + granular
8
+ * (one file, the in-flight task state), snapshot is manual + coarse
9
+ * (full DB + files).
10
+ *
11
+ * Storage: .matrixos/checkpoints/<taskId>.json (last write wins).
12
+ */
13
+ export type CheckpointState = {
14
+ taskId: string;
15
+ tool: string;
16
+ sessionID: string;
17
+ startedAt: number;
18
+ updatedAt: number;
19
+ step: number;
20
+ totalSteps: number;
21
+ history: {
22
+ tool: string;
23
+ args: Record<string, unknown>;
24
+ at: number;
25
+ ok: boolean;
26
+ }[];
27
+ context: Record<string, unknown>;
28
+ };
29
+ /** Start checkpointing a new task. Replaces any prior current task. */
30
+ export declare function startCheckpoint(taskId: string, tool: string, sessionID: string, totalSteps?: number, context?: Record<string, unknown>): void;
31
+ /** Record that a tool call happened. */
32
+ export declare function recordStep(tool: string, args: Record<string, unknown>, ok: boolean): void;
33
+ /** Update free-form context (e.g. current goal, partial results). */
34
+ export declare function updateContext(patch: Record<string, unknown>): void;
35
+ /** Force a flush to disk (in addition to the 30s timer). */
36
+ export declare function flush(): Promise<void>;
37
+ /** Stop the current checkpoint (call when the task completes). */
38
+ export declare function stopCheckpoint(): void;
39
+ /** Read a saved checkpoint by taskId, if it exists. */
40
+ export declare function loadCheckpoint(taskId: string, dir?: string): Promise<CheckpointState | null>;
41
+ /** List all checkpoints (for crash-recovery on session start). */
42
+ export declare function listCheckpoints(dir?: string): Promise<string[]>;
@@ -0,0 +1,18 @@
1
+ /**
2
+ * Graceful Degradation / Retry with Backoff (ANNEXE A14).
3
+ *
4
+ * Generic retry wrapper with exponential backoff + jitter.
5
+ * Used by the model resolver to retry on transient failures
6
+ * (rate-limit, network blip, 5xx) before falling back to the
7
+ * next provider in the chain.
8
+ */
9
+ export type RetryOptions = {
10
+ maxAttempts: number;
11
+ baseDelayMs: number;
12
+ maxDelayMs: number;
13
+ jitterRatio: number;
14
+ shouldRetry: (err: unknown) => boolean;
15
+ };
16
+ export declare function withRetry<T>(fn: () => Promise<T>, opts?: Partial<RetryOptions>): Promise<T>;
17
+ /** Default shouldRetry: retry on transient errors, NOT on validation/programmer errors. */
18
+ export declare function isTransientError(err: unknown): boolean;
@@ -0,0 +1,32 @@
1
+ /**
2
+ * Prompt Injection Defense (ANNEXE A12).
3
+ *
4
+ * Three defense layers applied at the tool boundary:
5
+ * 1. containsInjection() — detect known prompt-injection patterns in content
6
+ * the agent is about to consume (file contents, command output, etc.).
7
+ * 2. extractCanary() — pull a canary token from the system prompt and let
8
+ * checkCanaryInOutput() flag any tool output that contains it (a sign
9
+ * that the system prompt was exfiltrated into a tool result).
10
+ * 3. sanitizeToolArgs() — reject free-form text in tool arg fields that
11
+ * should be strictly typed (paths, IDs, enums).
12
+ */
13
+ export type InjectionMatch = {
14
+ id: string;
15
+ description: string;
16
+ snippet: string;
17
+ };
18
+ /** Returns the first injection match found, or null if clean. */
19
+ export declare function containsInjection(text: string): InjectionMatch | null;
20
+ /** Returns the canary token to embed in the system prompt. */
21
+ export declare function generateCanary(): string;
22
+ /** Returns true if the canary appears in the output (sign of exfiltration). */
23
+ export declare function checkCanaryInOutput(output: string, canary: string): boolean;
24
+ export type SanitizeResult = {
25
+ ok: true;
26
+ } | {
27
+ ok: false;
28
+ reason: string;
29
+ field: string;
30
+ };
31
+ /** Reject tool args that don't match the strict schema (no control chars in paths, etc.). */
32
+ export declare function sanitizeToolArgs(tool: string, args: Record<string, unknown>): SanitizeResult;
@@ -0,0 +1,25 @@
1
+ /**
2
+ * Prepare-Never-Submit (P-001) — tool-level validation guard.
3
+ *
4
+ * Inspired by ANNEXE_A A3. Detects high-risk tool calls BEFORE execution and
5
+ * throws so the agent sees the error and asks the human to confirm.
6
+ *
7
+ * NOTE: The annex proposes a skill-level frontmatter (requires_validation).
8
+ * The current OpenCode hook system does not expose skill context at the
9
+ * tool.execute.before layer, so this guard is PATTERN-based on tool + args.
10
+ * This is less fine-grained but more robust (catches both skill-driven and
11
+ * ad-hoc risky calls) and applicable today without a hook refactor.
12
+ */
13
+ export type ToolCall = {
14
+ tool: string;
15
+ args: Record<string, unknown>;
16
+ };
17
+ export type ValidationDecision = {
18
+ verdict: "deny";
19
+ reason: string;
20
+ pattern: string;
21
+ } | {
22
+ verdict: "allow";
23
+ };
24
+ /** Decide whether the tool call needs human validation. Throws nothing — pure decision. */
25
+ export declare function checkValidation(tool: string, args: Record<string, unknown>): ValidationDecision;
@@ -0,0 +1,33 @@
1
+ /**
2
+ * Skill versioning.
3
+ *
4
+ * Convention: <root>/<skill-name>/<semver>/SKILL.md
5
+ * The active version is recorded in <root>/<skill-name>/active (a file
6
+ * whose content is the version string, e.g. "1.0.0").
7
+ *
8
+ * Why: when modifying a skill, the old version is preserved. The active
9
+ * pointer lets the runtime pick the current version without breaking
10
+ * the in-flight calls. Rollback = rewrite the active file.
11
+ */
12
+ export type SkillVersion = {
13
+ name: string;
14
+ version: string;
15
+ path: string;
16
+ };
17
+ export declare class SkillRegistry {
18
+ private root;
19
+ constructor(root: string);
20
+ /** All versions of a skill, newest first. */
21
+ list(name: string): Promise<SkillVersion[]>;
22
+ /** List all skills that have at least one versioned SKILL.md. */
23
+ listAll(): Promise<{
24
+ name: string;
25
+ versions: SkillVersion[];
26
+ }[]>;
27
+ /** Read the active version (or the latest if no active file). */
28
+ getActive(name: string): Promise<SkillVersion | null>;
29
+ /** Set the active version. */
30
+ setActive(name: string, version: string): Promise<void>;
31
+ /** Add a new version (mkdir + write SKILL.md content). */
32
+ addVersion(name: string, version: string, content: string): Promise<SkillVersion>;
33
+ }
@@ -0,0 +1,41 @@
1
+ /**
2
+ * Skill test runner.
3
+ *
4
+ * Convention: a skill may have a co-located <name>.test.ts (or
5
+ * __tests__/<name>.test.ts) that exercises the skill instructions. The
6
+ * runner discovers these files, parses the skill frontmatter, and exposes
7
+ * the metadata to the test as a fixture.
8
+ *
9
+ * Usage (CLI):
10
+ * matrixos skill test <name> # run a skill's tests
11
+ * matrixos skill test --all # run all skills' tests
12
+ *
13
+ * Usage (programmatic):
14
+ * import { runSkillTest, parseSkillFrontmatter } from "./skill-test-runner"
15
+ */
16
+ export type SkillFrontmatter = {
17
+ name: string;
18
+ description: string;
19
+ triggers: string[];
20
+ requiresValidation: boolean;
21
+ raw: Record<string, unknown>;
22
+ };
23
+ export declare function parseSkillFrontmatter(content: string): {
24
+ frontmatter: SkillFrontmatter;
25
+ body: string;
26
+ } | null;
27
+ /** Find the test file for a skill. */
28
+ export declare function findSkillTestFile(skillRoot: string, skillName: string): Promise<string | null>;
29
+ /** Run a single skill test (programmatic). Returns a result object. */
30
+ export declare function runSkillTest(skillRoot: string, skillName: string, testFn: (ctx: {
31
+ skill: SkillFrontmatter;
32
+ body: string;
33
+ }) => void | Promise<void>): Promise<{
34
+ ok: boolean;
35
+ error?: string;
36
+ }>;
37
+ /** Discover all skills with test files. */
38
+ export declare function discoverSkillTests(skillRoot: string): Promise<{
39
+ name: string;
40
+ testFile: string;
41
+ }[]>;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kl-c/matrixos",
3
- "version": "0.1.40",
3
+ "version": "0.2.0",
4
4
  "description": "MaTrixOS — Agentic OS for OpenCode. Personalizable, communicating, self-improving, resilient.",
5
5
  "main": "./dist/index.js",
6
6
  "types": "dist/index.d.ts",