{"_id":"@at-series/command-policy","_rev":"2-9ad7cf3e4d14487ae5db786d43d18c70","name":"@at-series/command-policy","dist-tags":{"latest":"0.1.1"},"versions":{"0.1.0":{"name":"@at-series/command-policy","version":"0.1.0","keywords":["command-policy","security","shell","sql","redis"],"license":"Apache-2.0","_id":"@at-series/command-policy@0.1.0","maintainers":[{"name":"xwaimt","email":"xwaimt@gmail.com"}],"dist":{"shasum":"03654c19087bff7e1d116444753d273833446cca","tarball":"https://registry.npmjs.org/@at-series/command-policy/-/command-policy-0.1.0.tgz","fileCount":35,"integrity":"sha512-vmeHTsbvdqT2Q5B44RKZKucmvYKu8SVKuw+Uiodn9uZn8igxzA3gPeBCkfYc35OYgebSHrbqOdGBW0bs9Er5Cg==","signatures":[{"sig":"MEQCIGAOYtUwAqPw+HW0P9d9WqUqY0JeWYoDeCgKHI6JY2AmAiAPI8qW1be9rPoB5brHp8wx+3CMA6D6/W4kwvh5eop1CQ==","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":4408467},"main":"./dist/index.cjs","type":"module","types":"./dist/index.d.ts","module":"./dist/index.js","engines":{"node":">=18"},"exports":{".":{"import":{"types":"./dist/index.d.ts","default":"./dist/index.js"},"require":{"types":"./dist/index.d.cts","default":"./dist/index.cjs"}},"./build":{"import":{"types":"./dist/build.d.ts","default":"./dist/build.js"},"require":{"types":"./dist/build.d.cts","default":"./dist/build.cjs"}},"./mysql":{"import":{"types":"./dist/mysql.d.ts","default":"./dist/mysql.js"},"require":{"types":"./dist/mysql.d.cts","default":"./dist/mysql.cjs"}},"./redis":{"import":{"types":"./dist/redis.d.ts","default":"./dist/redis.js"},"require":{"types":"./dist/redis.d.cts","default":"./dist/redis.cjs"}},"./shell":{"import":{"types":"./dist/shell.d.ts","default":"./dist/shell.js"},"require":{"types":"./dist/shell.d.cts","default":"./dist/shell.cjs"}},"./python":{"import":{"types":"./dist/python.d.ts","default":"./dist/python.js"},"require":{"types":"./dist/python.d.cts","default":"./dist/python.cjs"}},"./sqlite":{"import":{"types":"./dist/sqlite.d.ts","default":"./dist/sqlite.js"},"require":{"types":"./dist/sqlite.d.cts","default":"./dist/sqlite.cjs"}},"./package.json":"./package.json"},"scripts":{"test":"node --import tsx --test test/**/*.test.ts","build":"npm run build:js && npm run build:types && node scripts/verify-version.mjs","verify":"npm run typecheck && npm test && npm run test:build && npm run build && npm run test:package && npm run lint:package && npm run pack:check","prepack":"npm run build","release":"changeset publish","build:js":"node scripts/build.mjs","changeset":"changeset","typecheck":"tsc --noEmit","pack:check":"npm pack --dry-run","test:build":"node --test --test-concurrency=1 test/build/*.test.mjs","build:types":"tsc --project tsconfig.build.json && node scripts/build-cjs-types.mjs","lint:package":"publint --strict","test:package":"npm run test:package:runtime && npm run test:package:types","test:contracts":"node --import tsx --test test/contracts/**/*.test.ts","test:package:types":"tsc --project test/package/tsconfig.json","test:package:runtime":"node --test --test-concurrency=1 test/package/*.test.mjs"},"_npmUser":{"name":"xwaimt","email":"xwaimt@gmail.com"},"_npmVersion":"11.13.0","description":"UI-independent command policy contracts and analyzer entry points","directories":{},"sideEffects":false,"_nodeVersion":"24.16.0","publishConfig":{"access":"public"},"_hasShrinkwrap":false,"devDependencies":{"tsx":"^4.23.12","esbuild":"^0.28.2","publint":"^0.3.24","typescript":"7.0.2","@types/node":"^18.19.130","sqlite3-parser":"0.7.1","@changesets/cli":"2.29.8","node-sql-parser":"5.4.0","web-tree-sitter":"0.26.13","@vscode/tree-sitter-wasm":"0.3.1"},"_npmOperationalInternal":{"tmp":"tmp/command-policy_0.1.0_1787724741814_0.47244672980189084","host":"s3://npm-registry-packages-npm-production"}},"0.1.1":{"name":"@at-series/command-policy","version":"0.1.1","description":"UI-independent command policy contracts and analyzer entry points","license":"Apache-2.0","type":"module","sideEffects":false,"engines":{"node":">=18"},"publishConfig":{"access":"public"},"main":"./dist/index.cjs","module":"./dist/index.js","types":"./dist/index.d.ts","exports":{".":{"import":{"types":"./dist/index.d.ts","default":"./dist/index.js"},"require":{"types":"./dist/index.d.cts","default":"./dist/index.cjs"}},"./shell":{"import":{"types":"./dist/shell.d.ts","default":"./dist/shell.js"},"require":{"types":"./dist/shell.d.cts","default":"./dist/shell.cjs"}},"./python":{"import":{"types":"./dist/python.d.ts","default":"./dist/python.js"},"require":{"types":"./dist/python.d.cts","default":"./dist/python.cjs"}},"./sqlite":{"import":{"types":"./dist/sqlite.d.ts","default":"./dist/sqlite.js"},"require":{"types":"./dist/sqlite.d.cts","default":"./dist/sqlite.cjs"}},"./mysql":{"import":{"types":"./dist/mysql.d.ts","default":"./dist/mysql.js"},"require":{"types":"./dist/mysql.d.cts","default":"./dist/mysql.cjs"}},"./redis":{"import":{"types":"./dist/redis.d.ts","default":"./dist/redis.js"},"require":{"types":"./dist/redis.d.cts","default":"./dist/redis.cjs"}},"./build":{"import":{"types":"./dist/build.d.ts","default":"./dist/build.js"},"require":{"types":"./dist/build.d.cts","default":"./dist/build.cjs"}},"./package.json":"./package.json"},"scripts":{"build":"npm run build:js && npm run build:types && node scripts/verify-version.mjs","build:js":"node scripts/build.mjs","build:types":"tsc --project tsconfig.build.json && node scripts/build-cjs-types.mjs","typecheck":"tsc --noEmit","test":"node --import tsx --test test/**/*.test.ts","test:contracts":"node --import tsx --test test/contracts/**/*.test.ts","test:build":"node --test --test-concurrency=1 test/build/*.test.mjs","test:package":"npm run test:package:runtime && npm run test:package:types","test:package:runtime":"node --test --test-concurrency=1 test/package/*.test.mjs","test:package:types":"tsc --project test/package/tsconfig.json","lint:package":"publint --strict","pack:check":"npm pack --dry-run","prepack":"npm run build","changeset":"changeset","release":"changeset publish","verify":"npm run typecheck && npm test && npm run test:build && npm run build && npm run test:package && npm run lint:package && npm run pack:check"},"keywords":["command-policy","security","shell","sql","redis"],"devDependencies":{"@changesets/cli":"2.29.8","@types/node":"^18.19.130","@vscode/tree-sitter-wasm":"0.3.1","esbuild":"^0.28.2","node-sql-parser":"5.4.0","publint":"^0.3.24","sqlite3-parser":"0.7.1","tsx":"^4.23.12","typescript":"7.0.2","web-tree-sitter":"0.26.13"},"gitHead":"ac79c0c071910c0c57345e769d89e99fd47c4937","_id":"@at-series/command-policy@0.1.1","_nodeVersion":"24.16.0","_npmVersion":"11.13.0","dist":{"integrity":"sha512-dRl6eH5ufnkkXrB5MecT24J05D10CXl2eF7ij7WmlWA4dWWa7yWCQsS0oIV27LcJqy0yj6dkmIh6MK0OBJ7/VA==","shasum":"8608914313b18b21eaa198231c3d8341fde2991b","tarball":"https://registry.npmjs.org/@at-series/command-policy/-/command-policy-0.1.1.tgz","fileCount":38,"unpackedSize":3209359,"signatures":[{"keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U","sig":"MEUCIQDOR0xji+7BOwk+7WHDymXYp1VC81FRZ6weZw5Q0jzGMQIgCNQOZ3TTvbrRZT1jtJ/m6FePjCJQ5XMvC9752GFhWCM="}]},"_npmUser":{"name":"xwaimt","email":"xwaimt@gmail.com"},"directories":{},"maintainers":[{"name":"xwaimt","email":"xwaimt@gmail.com"}],"_npmOperationalInternal":{"host":"s3://npm-registry-packages-npm-production","tmp":"tmp/command-policy_0.1.1_1787889557796_0.428333394097423"},"_hasShrinkwrap":false}},"time":{"created":"2026-08-26T06:12:21.585Z","modified":"2026-08-28T03:59:18.202Z","0.1.0":"2026-08-26T06:12:21.989Z","0.1.1":"2026-08-28T03:59:18.008Z"},"license":"Apache-2.0","keywords":["command-policy","security","shell","sql","redis"],"description":"UI-independent command policy contracts and analyzer entry points","maintainers":[{"name":"xwaimt","email":"xwaimt@gmail.com"}],"readme":"# @at-series/command-policy\n\nUI-independent command policy for AT Series plugins.\n\nThis repository publishes **one** Apache-2.0 npm package. Analyzers decide\nwhether a command or payload is `allow`, `review`, or `deny`. They do not map\ntrust levels, show confirmation UI, write logs, or execute anything.\n\n当前状态：**0.1.1**。Shell / Python / SQLite / MySQL / Redis 分析器已落地，fail-closed，\n双模块 CJS/ESM。第一位消费者是 **AT Terminal MCP** 的 limited-trust\n`run_remote_command`。JumpServer 尚未接入。\n\n---\n\n## Why this package exists\n\nLimited-trust agent commands used to be a plugin-local shell lexer plus a\nblocklist. That rejected `# Purpose:` newlines, treated unknown binaries as\nsafe, and could not see into `python3 -c`, `sqlite3`, or `sudo` wrappers.\n\nThis package replaces that with deterministic parsers and command contracts:\n\n- Parse the **exact** text that would be executed (`sourceText`).\n- Prove ordinary reads before auto-allowing them.\n- Confirm (or deny) writes, controls, sensitive reads, and anything unknown.\n- Keep the same decision shape across Shell, Python, SQL, and Redis so more\n  than one plugin can share it.\n\nIt is **not** an OS sandbox. A malicious binary whose name looks like `ls`\nis out of scope. The policy only reasons about static, parseable source.\n\n## Install\n\nNode.js **>= 18**. No `vscode` runtime or peer dependency.\n\nPublished on npm under the `@at-series` org:\n\n```sh\nnpm install @at-series/command-policy@0.1.1\n```\n\nPlugins must pin an **exact** version. Do not use `^` or `~` for a security\nboundary.\n\n```json\n{\n  \"dependencies\": {\n    \"@at-series/command-policy\": \"0.1.1\"\n  }\n}\n```\n\nPackage page: https://www.npmjs.com/package/@at-series/command-policy\n\nThe public TypeScript contract is in [`docs/api.md`](./docs/api.md).\n\n## Package surface\n\n| Entry | Role |\n| --- | --- |\n| `@at-series/command-policy` | Decision types, fail-closed reason codes, `combinePolicyDecisions`, version metadata |\n| `@at-series/command-policy/shell` | Bash scripts; lazily embeds Python / SQLite / MySQL / Redis when those payloads are static |\n| `@at-series/command-policy/python` | Python snippets (`-c` and equivalent) |\n| `@at-series/command-policy/sqlite` | SQLite SQL and sqlite3 CLI dot-commands |\n| `@at-series/command-policy/mysql` | MySQL SQL |\n| `@at-series/command-policy/redis` | Redis commands / RESP arrays |\n| `@at-series/command-policy/build` | Build-time copy of the WASM allowlist only |\n\nDual **ESM and CJS**, with `.d.ts` / `.d.cts`. Parsers are bundled into the\npublished tarball; they are not runtime `dependencies`.\n\n## Usage\n\nPass the final command text. Do not rewrite `sourceText` after a decision.\nPass `cwd` separately; never concatenate it into the command.\n\n```ts\nimport { createShellPolicyEvaluator } from '@at-series/command-policy/shell';\n\nconst evaluator = createShellPolicyEvaluator();\nconst decision = await evaluator.evaluate({\n  sourceText: finalCommandText,\n  cwd,\n});\n\nswitch (decision.action) {\n  case 'allow':\n    break;\n  case 'review':\n  case 'deny':\n    // Plugin shows confirmation or refuses. This package does not.\n    break;\n}\n```\n\n`decision.evidence[].summary` is always redacted, plugin-controlled wording.\nIt never includes source text, cwd, or parser errors. Ranges are half-open\n`[start, end)` over the exact UTF-16 source (CRLF is two code units).\n\n### Bundled consumers (VS Code / Electron)\n\nDefault WASM resolution uses `import.meta.url` next to this package's\n`dist/assets/`. After you re-bundle into a single CJS file, that URL is\nwrong (or empty), so a bundled consumer must own two things: the\n`import.meta.url` definition and the WASM asset paths.\n\n#### 1. esbuild recipe (`banner` + `define` are mandatory for CJS)\n\n```js\nimport { build } from 'esbuild';\n\nawait build({\n  entryPoints: ['src/policy-runtime.ts'],\n  bundle: true,\n  platform: 'node',\n  format: 'cjs',\n  target: 'node18',\n  outfile: 'dist/policy-runtime.js',\n  banner: {\n    js: 'var __policyModuleUrl = require(\"node:url\").pathToFileURL(__filename).href;',\n  },\n  define: { 'import.meta.url': '__policyModuleUrl' },\n});\n```\n\n> **Warning: skipping `banner`/`define` fails silently.** There is no build\n> error and no runtime exception. `import.meta.url` becomes empty in CJS\n> output, the embedded Python evaluator fails to initialize, and **every\n> `python3 -c` payload silently fail-closes to `review`** instead of being\n> analyzed. Plain shell commands keep working, so the misconfiguration is\n> very hard to notice. Keep the smoke assertion from step 5 in your CI.\n\n#### 2. WASM assets\n\nCopy the allowlist at build time, then resolve bytes or absolute paths at\nruntime through `assetResolver`:\n\n```js\n// Build script, next to the esbuild call above:\nimport { copyPolicyAssets } from '@at-series/command-policy/build';\n\nawait copyPolicyAssets({ destinationDirectory: 'dist/policy-assets' });\n```\n\n```ts\n// Runtime (src/policy-runtime.ts). assetDir is the directory that\n// copyPolicyAssets populated, resolved relative to the bundled file\n// (__dirname works because the bundle above is CJS):\nimport { createShellPolicyEvaluator } from '@at-series/command-policy/shell';\nimport { readFile } from 'node:fs/promises';\nimport { join } from 'node:path';\n\nconst assetDir = join(__dirname, 'policy-assets');\n\nconst evaluator = createShellPolicyEvaluator({\n  assetResolver: (asset) => readFile(join(assetDir, asset.fileName)),\n});\n```\n\nAllowlisted files:\n\n- `web-tree-sitter.wasm`\n- `tree-sitter-bash.wasm`\n- `tree-sitter-python.wasm`\n\n##### Optional: dropping the Python grammar (size / accuracy trade-off)\n\n`copyPolicyAssets` accepts an `include` allowlist of asset ids. The\ndefault (no `include`) copies all three WASM files and keeps today's\nbehavior exactly. A plugin that never needs embedded Python analysis can\nskip `tree-sitter-python.wasm` (~447KB):\n\n```js\nawait copyPolicyAssets({\n  destinationDirectory: 'dist/policy-assets',\n  include: ['tree-sitter-runtime', 'tree-sitter-bash'],\n});\n```\n\nThe cost is accuracy, never safety: with the grammar missing, every\n`python3 -c` payload fail-closes to `review` (reason code\n`shell.embedded_python_review`) instead of being analyzed — it can never\nbecome a false `allow`. Commands without an embedded Python payload\n(`uptime`, pipelines, `mysql -e`, …) are unaffected. Plugins that need\nembedded Python analysis must not use this filter.\n\n`tree-sitter-runtime` and `tree-sitter-bash` are **hard dependencies of\nthe shell domain**: dropping either one makes every shell evaluation fail\nclosed to `review` (`policy.initialization_failed`) — safe, but useless.\nUnknown asset ids throw a `TypeError` at build time.\n\n#### 3. Byte-level vs execution-level lazy loading\n\nA single-file bundle inlines every lazily imported sibling module —\n`./python.js`, `./sqlite.js`, `./mysql.js`, `./redis.js`, and\n`./tree-sitter-runtime.js` — so a shell-only bundle grows to roughly\n1.29MB (mostly the MySQL parser). **Execution-level lazy loading still\nholds**: a command with no embedded payload never executes those modules'\ninitialization code. Only the byte boundary collapses.\n\nTo keep the byte boundary too (for example, to keep the MySQL parser out\nof a VSIX), pick one of:\n\n- Mark the five sibling files external and copy the matching\n  `dist/*.js` / `dist/*.cjs` files next to your bundle:\n\n```js\nawait build({\n  // ...same options as step 1...\n  external: [\n    '*/mysql.js',\n    '*/python.js',\n    '*/sqlite.js',\n    '*/redis.js',\n    '*/tree-sitter-runtime.js',\n  ],\n});\n```\n\n- Or emit ESM with `splitting: true` (esbuild code splitting does not\n  support CJS output).\n\n#### 4. Optional warmup\n\n`warmupShellPolicyEvaluator()` pre-initializes the Tree-sitter runtime and\nthe bash grammar so the first `evaluate()` pays no cold-start cost\n(~18–20ms measured). Call it on extension activation; failures may be\nignored because `evaluate()` fails closed to `review` on its own.\n\n```ts\nimport { warmupShellPolicyEvaluator } from '@at-series/command-policy/shell';\n\nvoid warmupShellPolicyEvaluator({ assetResolver }).catch(() => {});\n```\n\n#### 5. Post-bundle smoke assertion (consumer CI)\n\nRun this against the **bundled** output. The second assertion is the guard\nfor step 1: when `import.meta.url` is not defined, it reports `review`\ninstead of `allow`.\n\n```js\nconst evaluator = createShellPolicyEvaluator({ assetResolver });\nassert.equal((await evaluator.evaluate({ sourceText: 'uptime' })).action, 'allow');\nassert.equal(\n  (await evaluator.evaluate({ sourceText: 'python3 -c \"print(1)\"' })).action,\n  'allow', // review here means import.meta.url was not defined correctly\n);\n```\n\n## Decision model\n\n| Action | Meaning for a plugin |\n| --- | --- |\n| `allow` | Every reachable effect is a proven ordinary read |\n| `review` | Human should confirm (writes, sensitive reads, parse/init/limit/unknown) |\n| `deny` | Hard reject where the contract says so (e.g. blocking Redis) |\n\nAggregation is always **`deny > review > allow`**.\n`combinePolicyDecisions` picks the first strictest decision. A consumer may\nonly make an official result **stricter**.\n\nFail-closed reason codes:\n\n- `policy.analysis_unavailable`\n- `policy.initialization_failed`\n- `policy.parse_failed`\n- `policy.resource_limit_exceeded`\n- `policy.unknown_semantics`\n- `policy.invalid_decision`\n\nSchema version is `1.0.0`. Each decision also carries parser/rule versions\nfrom `POLICY_VERSION_METADATA`.\n\n## What the analyzers cover\n\n**Shell.** Tree-sitter Bash → policy IR. Observers such as `ls`, `ps`,\n`systemctl status`, `docker ps`, `curl` GET/HEAD without credentials can\n`allow`. Writes, service control, unknown binaries, substitutions that\nexecute, and sensitive paths (`/etc/shadow`, `.env`, SSH keys) are `review`.\nRecognized wrappers (`sudo`, `env`, `busybox`, `timeout`, `bash -c` with a\nstatic script) re-enter analysis instead of blindly blocking the wrapper\nname. `# Purpose:` comments are comments, never authority.\n\n**Python.** A strict expression/control-flow subset plus approved `sqlite3`\nusage. Dynamic code, unknown imports, file writes, and sensitive SQL are\n`review`.\n\n**SQLite / MySQL.** Ordinary `SELECT` / metadata can `allow`. Writes,\ncontrols, unknown functions, and secret-bearing tables or columns are\n`review`. sqlite3 CLI: `.tables` / `.schema` allow; `.backup` / `.import`\nreview.\n\n**Redis.** Non-blocking reads can `allow`. Writes and controls are `review`.\nKnown blocking commands (`BLPOP`, …) are `deny`.\n\nShell evaluation **lazily** loads the other analyzers only when a static\nembedded payload is present, so a plain `uptime` does not initialize SQL\nparsers.\n\n## Plugin responsibilities\n\nThis library only returns `PolicyDecision`. The plugin still owns:\n\n- Trust mapping (`none` / `policy` / `full` in AT Terminal)\n- Confirmation UI and cancellation\n- Execution\n- Logging (must not log raw `sourceText` from this package’s evidence)\n\nAT Terminal MCP loads the engine only under limited trust, from a separate\n`policy-runtime.js` entry so the agentless base VSIX stays empty of policy\ncode and WASM.\n\nJumpServer is **next phase**: `runTerminalCommand` should call `/shell` on\nthe same normalized text that will execute; `sendTerminalInput` stays\nalways-confirm. JumpServer has no trust levels today, so a shared `allow`\nmust not skip existing confirms until an explicit trust model exists.\n\n## Development\n\n```sh\nnpm install\nnpm run verify\n```\n\n`verify` runs typecheck, analyzer/replay/adversarial/fuzz tests, the dual\nbuild, clean-tarball consumer tests, publint, and `npm pack --dry-run`.\n\nCI (GitHub Actions) runs that matrix on Node **18 / 20 / 22**. Releases use\n[Changesets](https://github.com/changesets/changesets) and npm provenance on\n`main` when publishing credentials are configured.\n\n```sh\nnpx changeset\n```\n\n## License\n\nApache-2.0. Bundled parser notices (Tree-sitter, sqlite3-parser,\nnode-sql-parser) are in [`NOTICE`](./NOTICE).\n","readmeFilename":"README.md"}