@pugi/cli 0.1.0-beta.47 → 0.1.0-beta.48

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.
@@ -346,6 +346,93 @@ const BUILD_TEST_PREFIXES = [
346
346
  'tsc -p',
347
347
  'eslint',
348
348
  'prettier --check',
349
+ // P0 fix 2026-05-29 (#37 CRITICAL): customer-blocking gap surfaced
350
+ // during dogfood. Engine emitted `chmod +x build.sh`, `node script.js`,
351
+ // `python3 -m pytest`, `git status`, `pnpm build`, `docker ps`, etc.
352
+ // and the classifier returned `unknown` → permission matrix denied
353
+ // в bypassPermissions mode (which the customer expected to auto-allow
354
+ // basic dev tools). Customers could not run ANY real build/test/git
355
+ // workflow through Pugi.
356
+ //
357
+ // The prefixes below cover three classes of developer tooling that
358
+ // are always allowed in `auto`/`dontAsk`/`bypassPermissions` modes,
359
+ // `ask` in interactive modes, and `deny` in `plan` (read-only) mode:
360
+ // - Language runtimes: `node`, `python`, `python3`, `ruby`, etc.
361
+ // - Native build chains: `gcc`, `clang`, `cmake`, `rustc`, etc.
362
+ // - Container/k8s read-class: `docker ps/inspect/logs`, `kubectl get`.
363
+ //
364
+ // Destructive variants are already gated upstream by DESTRUCTIVE_PATTERNS
365
+ // (e.g. `docker system prune`, `kubectl delete --all`). The first-token
366
+ // gate in classifyComponent runs THIS list before the unknown fallback.
367
+ //
368
+ // Language runtime invocations (first-token match, with or without args).
369
+ 'node',
370
+ 'python',
371
+ 'python3',
372
+ 'ruby',
373
+ 'perl',
374
+ 'php',
375
+ 'deno',
376
+ 'bun',
377
+ 'tsx',
378
+ 'ts-node',
379
+ // Native build chains.
380
+ 'gcc',
381
+ 'g++',
382
+ 'clang',
383
+ 'clang++',
384
+ 'cmake',
385
+ 'rustc',
386
+ 'javac',
387
+ 'java',
388
+ // Container/k8s read-class (the destructive subcommands are pre-empted
389
+ // by DESTRUCTIVE_PATTERNS: `docker system prune`, `kubectl delete --all`,
390
+ // `kubectl delete namespace`).
391
+ 'docker ps',
392
+ 'docker images',
393
+ 'docker inspect',
394
+ 'docker logs',
395
+ 'docker version',
396
+ 'docker info',
397
+ 'docker exec',
398
+ 'docker run',
399
+ 'docker stop',
400
+ 'docker start',
401
+ 'docker restart',
402
+ 'docker rm',
403
+ 'docker rmi',
404
+ 'docker build',
405
+ 'docker tag',
406
+ 'docker compose',
407
+ 'docker-compose',
408
+ 'kubectl get',
409
+ 'kubectl describe',
410
+ 'kubectl logs',
411
+ 'kubectl exec',
412
+ 'kubectl apply',
413
+ 'kubectl create',
414
+ 'kubectl rollout',
415
+ 'kubectl port-forward',
416
+ 'kubectl config',
417
+ // Git read+write surface (network ops already handled by NETWORK_PREFIXES;
418
+ // destructive ops `reset --hard`/`clean -fdx`/`push --force` blocked above).
419
+ // Note: WRITE_WORKSPACE_PREFIXES already covers `git commit/add/checkout/...`.
420
+ // These entries handle plain `git rev-list`, `git cherry-pick`, `git worktree`,
421
+ // `git submodule`, etc that customer scripts commonly invoke.
422
+ 'git rev-list',
423
+ 'git cherry-pick',
424
+ 'git worktree',
425
+ 'git submodule',
426
+ 'git blame',
427
+ 'git describe',
428
+ 'git tag --list',
429
+ 'git tag -l',
430
+ 'git for-each-ref',
431
+ 'git ls-remote',
432
+ // gh CLI (GitHub). `gh repo delete` / `gh release delete` reach into
433
+ // network operations but are non-destructive for the local workspace.
434
+ // Permission matrix asks before allowing in auto.
435
+ 'gh',
349
436
  ];
350
437
  /** Single-token read-only commands. Argument-free entries match exact. */
351
438
  const READ_TOKENS = new Set([
@@ -384,6 +471,16 @@ const READ_TOKENS = new Set([
384
471
  'cut',
385
472
  'sort',
386
473
  'uniq',
474
+ // P0 fix 2026-05-29 (#37 CRITICAL): structured-data inspection tools
475
+ // are pure stdin/stdout transformers (no FS write, no network) when
476
+ // не paired с `>` redirection (the redirection branch above promotes
477
+ // к write_workspace independently). Common в dev scripts for parsing
478
+ // package.json, tsconfig.json, Helm values.yaml, etc.
479
+ // `tee` is INTENTIONALLY excluded — it writes by definition, even
480
+ // в protected paths (`tee /etc/...` is already in DESTRUCTIVE_PATTERNS).
481
+ 'jq',
482
+ 'yq',
483
+ 'column',
387
484
  ]);
388
485
  const READ_PREFIXES = [
389
486
  'git status',
@@ -418,6 +515,16 @@ const WRITE_WORKSPACE_PREFIXES = [
418
515
  'git tag',
419
516
  'git rebase',
420
517
  'git merge',
518
+ // P0 fix 2026-05-29 (#37 CRITICAL): file-permission ops are common
519
+ // в build scripts (`chmod +x build.sh`, `chown $USER file`). The
520
+ // destructive variants (`chmod 777 /`, `chmod -R 777 /`, `chmod -R
521
+ // 777 ~`, `chown -R root /`, `chown -R / ...`) are pre-empted by
522
+ // DESTRUCTIVE_PATTERNS which runs BEFORE this list — safe to add
523
+ // here for the non-destructive path. detectProtectedWrite's `\bchmod\b`
524
+ // / `\bchown\b` regex also catches writes into protected paths
525
+ // regardless of this list.
526
+ 'chmod ',
527
+ 'chown ',
421
528
  ];
422
529
  /**
423
530
  * Protected-write triggers. If a command writes to any of these paths
@@ -636,6 +743,25 @@ function classifyComponent(cmd, ctx) {
636
743
  if (trimmed === 'make' || trimmed.startsWith('make ')) {
637
744
  return { class: 'build_test', reason: 'make runner', matched: 'make' };
638
745
  }
746
+ // 7c. Operator-override safe tokens (P0 fix 2026-05-29 #37).
747
+ // `PUGI_CLASSIFIER_EXTRA_SAFE=tool1,tool2,...` extends the BUILD_TEST
748
+ // first-token list at runtime. This is a security-sensitive escape
749
+ // hatch — operators can add their custom build tools without a
750
+ // recompile. Destructive patterns ALREADY ran above (step 1) so this
751
+ // cannot whitelist `rm`, `mkfs`, `git push --force`, etc. The match
752
+ // is strict first-token equality — not substring — and the env var
753
+ // is read fresh on every classify call so tests can mutate it.
754
+ const extraSafe = readExtraSafeTokens();
755
+ if (extraSafe.size > 0) {
756
+ const firstTokenForExtraSafe = trimmed.split(/\s+/)[0] ?? '';
757
+ if (extraSafe.has(firstTokenForExtraSafe)) {
758
+ return {
759
+ class: 'build_test',
760
+ reason: `PUGI_CLASSIFIER_EXTRA_SAFE override: ${firstTokenForExtraSafe}`,
761
+ matched: firstTokenForExtraSafe,
762
+ };
763
+ }
764
+ }
639
765
  // 7c. Bare `cd <path>` (inside workspace — the cwd-escape detector
640
766
  // upgrades the class to write_protected when the target is
641
767
  // outside). Standalone `cd` (HOME) is escape, also handled by the
@@ -756,6 +882,40 @@ function nestingDepth(cmd, open, close) {
756
882
  function escapeRegex(s) {
757
883
  return s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
758
884
  }
885
+ /**
886
+ * Operator-override safe tokens. Read from `PUGI_CLASSIFIER_EXTRA_SAFE`
887
+ * (comma-separated). Allows operators to extend the BUILD_TEST first-
888
+ * token list at runtime for site-specific tooling без recompile.
889
+ *
890
+ * Security note: destructive substring patterns run BEFORE this gate
891
+ * (step 1 in classifyComponent), so this cannot whitelist `rm`, `mkfs`,
892
+ * `git push --force`, etc. The env var only adds tools to the benign
893
+ * build_test class. Invalid entries (empty strings, тokens containing
894
+ * shell metas) are silently dropped to avoid surprising classifications.
895
+ *
896
+ * Read fresh on every call so per-test mutations work и so operators
897
+ * can update without restarting the agent loop. The cost (one env var
898
+ * read + Set construction per call) is negligible for the classifier's
899
+ * call frequency.
900
+ */
901
+ function readExtraSafeTokens() {
902
+ const raw = process.env.PUGI_CLASSIFIER_EXTRA_SAFE;
903
+ if (!raw || raw.trim() === '')
904
+ return new Set();
905
+ const tokens = new Set();
906
+ for (const candidate of raw.split(',')) {
907
+ const trimmed = candidate.trim();
908
+ if (trimmed === '')
909
+ continue;
910
+ // Reject anything containing shell metas or whitespace — only bare
911
+ // tool names allowed. Defends against accidental
912
+ // `PUGI_CLASSIFIER_EXTRA_SAFE='rm -rf /'` smuggling.
913
+ if (/[\s;|&<>$`(){}\[\]'"\\]/.test(trimmed))
914
+ continue;
915
+ tokens.add(trimmed);
916
+ }
917
+ return tokens;
918
+ }
759
919
  function detectProtectedWrite(cmd, ctx) {
760
920
  // Surface every write target this command produces so we can both
761
921
  // protected-path-check and outside-workspace-check them uniformly.
@@ -44,7 +44,7 @@ export function sanitizeSemver(raw) {
44
44
  * during import). When bumping the CLI version BOTH literals must be
45
45
  * updated; the release smoke-test (`pack:smoke`) verifies they agree.
46
46
  */
47
- export const PUGI_CLI_VERSION = sanitizeSemver('0.1.0-beta.47');
47
+ export const PUGI_CLI_VERSION = sanitizeSemver('0.1.0-beta.48');
48
48
  /**
49
49
  * Outbound: the CLI's installed semver. Read at request time by
50
50
  * `version-interceptor.ts` and injected on every `fetch` call.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pugi/cli",
3
- "version": "0.1.0-beta.47",
3
+ "version": "0.1.0-beta.48",
4
4
  "description": "Pugi CLI - terminal-native software execution system",
5
5
  "homepage": "https://pugi.io",
6
6
  "repository": {
@@ -55,7 +55,7 @@
55
55
  "undici": "^8.3.0",
56
56
  "zod": "^3.23.0",
57
57
  "@pugi/personas": "0.1.2",
58
- "@pugi/sdk": "0.1.0-beta.47"
58
+ "@pugi/sdk": "0.1.0-beta.48"
59
59
  },
60
60
  "devDependencies": {
61
61
  "@types/node": "^22.0.0",