@warlock.js/ai-workspace 5.43.0 → 5.44.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.
Files changed (84) hide show
  1. package/CHANGELOG.md +18 -0
  2. package/README.md +14 -1
  3. package/cjs/index.cjs +1781 -141
  4. package/cjs/index.cjs.map +1 -1
  5. package/esm/backends/local.d.mts.map +1 -1
  6. package/esm/backends/local.mjs +36 -22
  7. package/esm/backends/local.mjs.map +1 -1
  8. package/esm/contracts/index.d.mts +5 -1
  9. package/esm/contracts/shell-approval.type.d.mts +78 -0
  10. package/esm/contracts/shell-approval.type.d.mts.map +1 -0
  11. package/esm/contracts/tool-io.type.d.mts +35 -1
  12. package/esm/contracts/tool-io.type.d.mts.map +1 -1
  13. package/esm/contracts/workspace-backend.contract.d.mts +11 -1
  14. package/esm/contracts/workspace-backend.contract.d.mts.map +1 -1
  15. package/esm/contracts/workspace-edit.type.d.mts +66 -0
  16. package/esm/contracts/workspace-edit.type.d.mts.map +1 -0
  17. package/esm/contracts/workspace-ops.contract.d.mts +7 -1
  18. package/esm/contracts/workspace-ops.contract.d.mts.map +1 -1
  19. package/esm/contracts/workspace-output.type.d.mts +57 -0
  20. package/esm/contracts/workspace-output.type.d.mts.map +1 -0
  21. package/esm/contracts/workspace-policy.type.d.mts +38 -1
  22. package/esm/contracts/workspace-policy.type.d.mts.map +1 -1
  23. package/esm/contracts/workspace.contract.d.mts +11 -0
  24. package/esm/contracts/workspace.contract.d.mts.map +1 -1
  25. package/esm/edit/apply-replacement.mjs +58 -0
  26. package/esm/edit/apply-replacement.mjs.map +1 -0
  27. package/esm/edit/match.d.mts +16 -0
  28. package/esm/edit/match.d.mts.map +1 -0
  29. package/esm/edit/match.mjs +214 -0
  30. package/esm/edit/match.mjs.map +1 -0
  31. package/esm/errors.d.mts +12 -1
  32. package/esm/errors.d.mts.map +1 -1
  33. package/esm/errors.mjs +1 -0
  34. package/esm/errors.mjs.map +1 -1
  35. package/esm/hooks/after-edit.mjs +36 -0
  36. package/esm/hooks/after-edit.mjs.map +1 -0
  37. package/esm/index.d.mts +7 -2
  38. package/esm/index.mjs +3 -2
  39. package/esm/ops.d.mts +3 -1
  40. package/esm/ops.d.mts.map +1 -1
  41. package/esm/ops.mjs +196 -75
  42. package/esm/ops.mjs.map +1 -1
  43. package/esm/output/spill-dir.mjs +15 -0
  44. package/esm/output/spill-dir.mjs.map +1 -0
  45. package/esm/output/spill.mjs +220 -0
  46. package/esm/output/spill.mjs.map +1 -0
  47. package/esm/policy/policy.d.mts +3 -1
  48. package/esm/policy/policy.d.mts.map +1 -1
  49. package/esm/policy/policy.mjs +12 -4
  50. package/esm/policy/policy.mjs.map +1 -1
  51. package/esm/policy/powershell-aliases.mjs +275 -0
  52. package/esm/policy/powershell-aliases.mjs.map +1 -0
  53. package/esm/policy/powershell-parser.mjs +418 -0
  54. package/esm/policy/powershell-parser.mjs.map +1 -0
  55. package/esm/policy/shell-check.d.mts +32 -2
  56. package/esm/policy/shell-check.d.mts.map +1 -1
  57. package/esm/policy/shell-check.mjs +183 -31
  58. package/esm/policy/shell-check.mjs.map +1 -1
  59. package/esm/policy/shell-interpreter.d.mts +45 -0
  60. package/esm/policy/shell-interpreter.d.mts.map +1 -0
  61. package/esm/policy/shell-interpreter.mjs +109 -0
  62. package/esm/policy/shell-interpreter.mjs.map +1 -0
  63. package/esm/policy/shell-parser.d.mts +31 -0
  64. package/esm/policy/shell-parser.d.mts.map +1 -0
  65. package/esm/policy/shell-parser.mjs +3 -0
  66. package/esm/policy/shell-parser.mjs.map +1 -1
  67. package/esm/tools/glob.mjs +1 -0
  68. package/esm/tools/glob.mjs.map +1 -1
  69. package/esm/tools/run-shell.d.mts +10 -0
  70. package/esm/tools/run-shell.d.mts.map +1 -1
  71. package/esm/tools/run-shell.mjs +18 -1
  72. package/esm/tools/run-shell.mjs.map +1 -1
  73. package/esm/tools/run-tests.d.mts +7 -0
  74. package/esm/tools/run-tests.d.mts.map +1 -1
  75. package/esm/tools/run-tests.mjs +5 -4
  76. package/esm/tools/run-tests.mjs.map +1 -1
  77. package/esm/tools/write-file.mjs +3 -2
  78. package/esm/tools/write-file.mjs.map +1 -1
  79. package/esm/workspace.d.mts.map +1 -1
  80. package/esm/workspace.mjs +16 -5
  81. package/esm/workspace.mjs.map +1 -1
  82. package/llms-full.txt +64 -17
  83. package/package.json +3 -3
  84. package/skills/use-a-workspace/SKILL.md +64 -17
@@ -1 +1 @@
1
- {"version":3,"file":"policy.mjs","names":[],"sources":["../../../../../../../../packages/ai-workspace/src/policy/policy.ts"],"sourcesContent":["import path from \"node:path\";\nimport { realpath } from \"node:fs/promises\";\nimport { WorkspacePolicyError } from \"../errors\";\nimport type { WorkspacePolicy } from \"../contracts\";\nimport { isInside, matchesAnyGlob } from \"./jail\";\nimport { checkShellCommand } from \"./shell-check\";\n\n/**\n * The outcome of resolving a workspace-relative (or absolute) input path\n * against the jail — the canonical absolute location the backend should\n * touch, plus the workspace-relative form (POSIX-style, `/`-separated)\n * the agent and tool results echo back.\n */\nexport interface ResolvedPath {\n /** Canonical absolute path (symlinks in existing ancestors collapsed). */\n absolutePath: string;\n /**\n * The path relative to `policy.cwd`, `/`-separated regardless of OS,\n * used in tool results so the agent always sees stable workspace paths.\n * Empty string when the resolved path IS the jail root.\n */\n relativePath: string;\n}\n\n/**\n * Resolve the canonical absolute form of `target`, collapsing any\n * symlinks. `target` may not yet exist (a fresh `writeFile`/`mkdir`),\n * so we realpath the deepest **existing** ancestor and re-attach the\n * non-existent tail — a symlinked ancestor still cannot smuggle the\n * path out of the jail, while genuinely new leaves stay creatable.\n */\nasync function canonicalize(target: string): Promise<string> {\n let resolvedTarget = path.resolve(target);\n const tail: string[] = [];\n\n // Walk up until an existing ancestor is found (or we hit the root).\n // eslint-disable-next-line no-constant-condition\n while (true) {\n try {\n const real = await realpath(resolvedTarget);\n\n return tail.length > 0 ? path.join(real, ...tail) : real;\n } catch (error) {\n const code = (error as NodeJS.ErrnoException).code;\n\n // Only ENOENT means \"this segment doesn't exist yet\" — keep\n // walking up. Any other error (EACCES, ELOOP, …) is a real fault.\n if (code !== \"ENOENT\") {\n throw error;\n }\n\n const parent = path.dirname(resolvedTarget);\n\n // Reached the filesystem root without finding an existing\n // ancestor — give back the lexically-resolved path unchanged.\n if (parent === resolvedTarget) {\n return path.join(resolvedTarget, ...tail);\n }\n\n tail.unshift(path.basename(resolvedTarget));\n resolvedTarget = parent;\n }\n }\n}\n\n/**\n * Resolve and jail a single input path against a {@link WorkspacePolicy}.\n *\n * The input is resolved against `policy.cwd`, canonicalized (symlinks in\n * existing ancestors collapsed so a symlinked directory cannot escape\n * the jail), then accepted **only** when it sits under `cwd` or one of\n * the `allowPaths` roots. A path that escapes, or that matches any\n * `denyPaths` glob even while inside `cwd`, is rejected with a\n * {@link WorkspacePolicyError} of type `\"path-escape\"`.\n *\n * Why this does not use `@warlock.js/fs`'s confined reader (`fs.confined`):\n * the two answer different questions. The confined reader refuses absolute\n * paths, refuses every symbolic link (or follows only links inside its one\n * root), requires the path to exist, and reads the file itself through a\n * no-follow, identity-checked open. This jail must instead accept absolute\n * paths inside `cwd`, several `allowPaths` roots, `denyPaths` globs, a\n * symlinked `cwd`, links whose target stays inside, and paths that do not\n * exist yet (`writeFile`, `mkdir`), and it returns a path that writes,\n * `exec`'s working directory and redirections use afterwards. Switching would\n * change what agents may touch, so the workspace keeps this resolver.\n *\n * @param policy - The bounding policy (its `cwd` is the jail root).\n * @param inputPath - A workspace-relative or absolute path to resolve.\n * @returns The canonical absolute path plus its `/`-separated relative form.\n * @throws {WorkspacePolicyError} When the path escapes the jail or hits a deny glob.\n *\n * @example\n * const { absolutePath } = await resolveInJail(policy, \"src/index.ts\");\n */\nexport async function resolveInJail(\n policy: WorkspacePolicy,\n inputPath: string,\n): Promise<ResolvedPath> {\n const jailRoot = await canonicalize(policy.cwd);\n const requested = path.isAbsolute(inputPath) ? inputPath : path.join(policy.cwd, inputPath);\n const absolutePath = await canonicalize(requested);\n\n const insideCwd = isInside(absolutePath, jailRoot);\n const allowRoots = policy.allowPaths ?? [];\n let insideAllow = false;\n\n if (!insideCwd) {\n for (const root of allowRoots) {\n const canonicalRoot = await canonicalize(root);\n\n if (isInside(absolutePath, canonicalRoot)) {\n insideAllow = true;\n\n break;\n }\n }\n }\n\n if (!insideCwd && !insideAllow) {\n throw new WorkspacePolicyError(`Path \"${inputPath}\" resolves outside the workspace jail.`, {\n type: \"path-escape\",\n path: inputPath,\n });\n }\n\n // `denyPaths` is evaluated relative to the jail root and wins even\n // when the path is comfortably inside `cwd`.\n const relativeToJail = insideCwd\n ? path.relative(jailRoot, absolutePath).split(path.sep).join(\"/\")\n : \"\";\n\n if (insideCwd && policy.denyPaths && policy.denyPaths.length > 0) {\n if (matchesAnyGlob(relativeToJail, policy.denyPaths)) {\n throw new WorkspacePolicyError(`Path \"${inputPath}\" is blocked by the workspace deny list.`, {\n type: \"path-escape\",\n path: inputPath,\n });\n }\n }\n\n return { absolutePath, relativePath: relativeToJail };\n}\n\n/**\n * Whether a command line is permitted by the policy's `shell` sub-policy —\n * `checkShellCommand(policy, command).allowed`.\n *\n * In argv mode (the default) the command must tokenize into a single argv\n * (no unquoted `;`, `&&`, `|`, backticks, `$`, redirection, …): the backend\n * spawns it with no shell, and unquoted metacharacters were exactly how an\n * injected command chain used to ride past the allowlist. In shell mode\n * (`interpreter: \"sh\"`) every simple command in the line is checked. Each\n * argv's executable basename is matched against `shell.deny` (wins, case\n * insensitive) then `shell.allow` (exhaustive when non-empty), then\n * `shell.denyPatterns` and `shell.allowCommand`. An absent `shell` block\n * means no command may run at all.\n *\n * Returns a plain `boolean`; use {@link checkShellCommand} for the reason\n * and the model-facing message. Redirection targets are checked lexically\n * here; `exec` re-checks them with symlinks resolved.\n *\n * @example\n * isCommandAllowed({ cwd, shell: { allow: [\"npm\"] } }, \"npm test\"); // true\n * isCommandAllowed({ cwd, shell: { allow: [\"npm\"] } }, \"rm -rf /\"); // false\n * isCommandAllowed({ cwd, shell: { allow: [\"npm\"] } }, \"npm test; rm -rf /\"); // false\n */\nexport function isCommandAllowed(policy: WorkspacePolicy, command: string): boolean {\n return checkShellCommand(policy, command).allowed;\n}\n\n/**\n * Build the exact environment a spawned process receives — `process.env`\n * is **never** inherited wholesale. The result is\n * `{ ...pick(process.env, inheritEnv), ...shell.env }`, so a command\n * cannot see `PATH` (and thus often cannot find `node`/`npm`) unless the\n * policy opts in via `shell.inheritEnv: [\"PATH\"]`. Explicit `shell.env`\n * values override inherited ones on key collision.\n *\n * @example\n * buildEnv({ cwd, shell: { inheritEnv: [\"PATH\"], env: { CI: \"1\" } } });\n * // → { PATH: <process PATH>, CI: \"1\" }\n */\nexport function buildEnv(policy: WorkspacePolicy): Record<string, string> {\n const shell = policy.shell;\n const env: Record<string, string> = {};\n\n if (!shell) {\n return env;\n }\n\n for (const key of shell.inheritEnv ?? []) {\n const value = process.env[key];\n\n if (value !== undefined) {\n env[key] = value;\n }\n }\n\n if (shell.env) {\n for (const [key, value] of Object.entries(shell.env)) {\n env[key] = value;\n }\n }\n\n return env;\n}\n\n/**\n * Variables removed from the environment of a shell-mode (`interpreter:\n * \"sh\"`) command: each changes how the shell itself starts, parses or\n * resolves the line (`ENV`/`BASH_ENV` run a startup file, `SHELLOPTS` +\n * `PS4` can run command substitutions, `BASH_FUNC_*` imports functions that\n * shadow commands, `CDPATH` moves `cd`, `IFS` re-splits words).\n */\nconst SHELL_STARTUP_VARIABLES = new Set([\n \"ENV\",\n \"BASH_ENV\",\n \"SHELLOPTS\",\n \"BASHOPTS\",\n \"CDPATH\",\n \"IFS\",\n \"PS4\",\n \"GLOBIGNORE\",\n]);\n\n/**\n * The environment a command actually receives: {@link buildEnv}, minus the\n * shell start-up variables when the policy runs lines through a shell.\n * Compared case-insensitively, because Windows environment names are.\n */\nexport function buildCommandEnv(policy: WorkspacePolicy): Record<string, string> {\n const env = buildEnv(policy);\n\n if ((policy.shell?.interpreter ?? \"none\") !== \"sh\") {\n return env;\n }\n\n for (const key of Object.keys(env)) {\n const upper = key.toUpperCase();\n\n if (SHELL_STARTUP_VARIABLES.has(upper) || upper.startsWith(\"BASH_FUNC_\")) {\n delete env[key];\n }\n }\n\n return env;\n}\n"],"mappings":";;;;;;;;;;;;;;AA+BA,eAAe,aAAa,QAAiC;CAC3D,IAAI,iBAAiB,KAAK,QAAQ,MAAM;CACxC,MAAM,OAAiB,CAAC;CAIxB,OAAO,MACL,IAAI;EACF,MAAM,OAAO,MAAM,SAAS,cAAc;EAE1C,OAAO,KAAK,SAAS,IAAI,KAAK,KAAK,MAAM,GAAG,IAAI,IAAI;CACtD,SAAS,OAAO;EAKd,IAJc,MAAgC,SAIjC,UACX,MAAM;EAGR,MAAM,SAAS,KAAK,QAAQ,cAAc;EAI1C,IAAI,WAAW,gBACb,OAAO,KAAK,KAAK,gBAAgB,GAAG,IAAI;EAG1C,KAAK,QAAQ,KAAK,SAAS,cAAc,CAAC;EAC1C,iBAAiB;CACnB;AAEJ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA+BA,eAAsB,cACpB,QACA,WACuB;CACvB,MAAM,WAAW,MAAM,aAAa,OAAO,GAAG;CAE9C,MAAM,eAAe,MAAM,aADT,KAAK,WAAW,SAAS,IAAI,YAAY,KAAK,KAAK,OAAO,KAAK,SAAS,CACzC;CAEjD,MAAM,YAAY,SAAS,cAAc,QAAQ;CACjD,MAAM,aAAa,OAAO,cAAc,CAAC;CACzC,IAAI,cAAc;CAElB,IAAI,CAAC,WACH,KAAK,MAAM,QAAQ,YAAY;EAC7B,MAAM,gBAAgB,MAAM,aAAa,IAAI;EAE7C,IAAI,SAAS,cAAc,aAAa,GAAG;GACzC,cAAc;GAEd;EACF;CACF;CAGF,IAAI,CAAC,aAAa,CAAC,aACjB,MAAM,IAAI,qBAAqB,SAAS,UAAU,yCAAyC;EACzF,MAAM;EACN,MAAM;CACR,CAAC;CAKH,MAAM,iBAAiB,YACnB,KAAK,SAAS,UAAU,YAAY,CAAC,CAAC,MAAM,KAAK,GAAG,CAAC,CAAC,KAAK,GAAG,IAC9D;CAEJ,IAAI,aAAa,OAAO,aAAa,OAAO,UAAU,SAAS,GAC7D;MAAI,eAAe,gBAAgB,OAAO,SAAS,GACjD,MAAM,IAAI,qBAAqB,SAAS,UAAU,2CAA2C;GAC3F,MAAM;GACN,MAAM;EACR,CAAC;CACH;CAGF,OAAO;EAAE;EAAc,cAAc;CAAe;AACtD;;;;;;;;;;;;;;;;;;;;;;;;AAyBA,SAAgB,iBAAiB,QAAyB,SAA0B;CAClF,OAAO,kBAAkB,QAAQ,OAAO,CAAC,CAAC;AAC5C;;;;;;;;;;;;;AAcA,SAAgB,SAAS,QAAiD;CACxE,MAAM,QAAQ,OAAO;CACrB,MAAM,MAA8B,CAAC;CAErC,IAAI,CAAC,OACH,OAAO;CAGT,KAAK,MAAM,OAAO,MAAM,cAAc,CAAC,GAAG;EACxC,MAAM,QAAQ,QAAQ,IAAI;EAE1B,IAAI,UAAU,QACZ,IAAI,OAAO;CAEf;CAEA,IAAI,MAAM,KACR,KAAK,MAAM,CAAC,KAAK,UAAU,OAAO,QAAQ,MAAM,GAAG,GACjD,IAAI,OAAO;CAIf,OAAO;AACT;;;;;;;;AASA,MAAM,0CAA0B,IAAI,IAAI;CACtC;CACA;CACA;CACA;CACA;CACA;CACA;CACA;AACF,CAAC;;;;;;AAOD,SAAgB,gBAAgB,QAAiD;CAC/E,MAAM,MAAM,SAAS,MAAM;CAE3B,KAAK,OAAO,OAAO,eAAe,YAAY,MAC5C,OAAO;CAGT,KAAK,MAAM,OAAO,OAAO,KAAK,GAAG,GAAG;EAClC,MAAM,QAAQ,IAAI,YAAY;EAE9B,IAAI,wBAAwB,IAAI,KAAK,KAAK,MAAM,WAAW,YAAY,GACrE,OAAO,IAAI;CAEf;CAEA,OAAO;AACT"}
1
+ {"version":3,"file":"policy.mjs","names":[],"sources":["../../../../../../../../packages/ai-workspace/src/policy/policy.ts"],"sourcesContent":["import path from \"node:path\";\nimport { realpath } from \"node:fs/promises\";\nimport { WorkspacePolicyError } from \"../errors\";\nimport type { WorkspacePolicy } from \"../contracts\";\nimport { isInside, matchesAnyGlob } from \"./jail\";\nimport { resolveSpillDir } from \"../output/spill-dir\";\nimport { checkShellCommand } from \"./shell-check\";\n\n/**\n * The outcome of resolving a workspace-relative (or absolute) input path\n * against the jail — the canonical absolute location the backend should\n * touch, plus the workspace-relative form (POSIX-style, `/`-separated)\n * the agent and tool results echo back.\n */\nexport interface ResolvedPath {\n /** Canonical absolute path (symlinks in existing ancestors collapsed). */\n absolutePath: string;\n /**\n * The path relative to `policy.cwd`, `/`-separated regardless of OS,\n * used in tool results so the agent always sees stable workspace paths.\n * Empty string when the resolved path IS the jail root.\n */\n relativePath: string;\n}\n\n/**\n * Resolve the canonical absolute form of `target`, collapsing any\n * symlinks. `target` may not yet exist (a fresh `writeFile`/`mkdir`),\n * so we realpath the deepest **existing** ancestor and re-attach the\n * non-existent tail — a symlinked ancestor still cannot smuggle the\n * path out of the jail, while genuinely new leaves stay creatable.\n */\nasync function canonicalize(target: string): Promise<string> {\n let resolvedTarget = path.resolve(target);\n const tail: string[] = [];\n\n // Walk up until an existing ancestor is found (or we hit the root).\n // eslint-disable-next-line no-constant-condition\n while (true) {\n try {\n const real = await realpath(resolvedTarget);\n\n return tail.length > 0 ? path.join(real, ...tail) : real;\n } catch (error) {\n const code = (error as NodeJS.ErrnoException).code;\n\n // Only ENOENT means \"this segment doesn't exist yet\" — keep\n // walking up. Any other error (EACCES, ELOOP, …) is a real fault.\n if (code !== \"ENOENT\") {\n throw error;\n }\n\n const parent = path.dirname(resolvedTarget);\n\n // Reached the filesystem root without finding an existing\n // ancestor — give back the lexically-resolved path unchanged.\n if (parent === resolvedTarget) {\n return path.join(resolvedTarget, ...tail);\n }\n\n tail.unshift(path.basename(resolvedTarget));\n resolvedTarget = parent;\n }\n }\n}\n\n/**\n * Resolve and jail a single input path against a {@link WorkspacePolicy}.\n *\n * The input is resolved against `policy.cwd`, canonicalized (symlinks in\n * existing ancestors collapsed so a symlinked directory cannot escape\n * the jail), then accepted **only** when it sits under `cwd` or one of\n * the `allowPaths` roots. A path that escapes, or that matches any\n * `denyPaths` glob even while inside `cwd`, is rejected with a\n * {@link WorkspacePolicyError} of type `\"path-escape\"`.\n *\n * Why this does not use `@warlock.js/fs`'s confined reader (`fs.confined`):\n * the two answer different questions. The confined reader refuses absolute\n * paths, refuses every symbolic link (or follows only links inside its one\n * root), requires the path to exist, and reads the file itself through a\n * no-follow, identity-checked open. This jail must instead accept absolute\n * paths inside `cwd`, several `allowPaths` roots, `denyPaths` globs, a\n * symlinked `cwd`, links whose target stays inside, and paths that do not\n * exist yet (`writeFile`, `mkdir`), and it returns a path that writes,\n * `exec`'s working directory and redirections use afterwards. Switching would\n * change what agents may touch, so the workspace keeps this resolver.\n *\n * @param policy - The bounding policy (its `cwd` is the jail root).\n * @param inputPath - A workspace-relative or absolute path to resolve.\n * @param access - `\"read\"` also accepts the `output.spillDir` directory (only\n * `readFile` asks for it); `\"write\"` (default) never does.\n * @returns The canonical absolute path plus its `/`-separated relative form.\n * @throws {WorkspacePolicyError} When the path escapes the jail or hits a deny glob.\n *\n * @example\n * const { absolutePath } = await resolveInJail(policy, \"src/index.ts\");\n */\nexport async function resolveInJail(\n policy: WorkspacePolicy,\n inputPath: string,\n access: \"read\" | \"write\" = \"write\",\n): Promise<ResolvedPath> {\n const jailRoot = await canonicalize(policy.cwd);\n const requested = path.isAbsolute(inputPath) ? inputPath : path.join(policy.cwd, inputPath);\n const absolutePath = await canonicalize(requested);\n\n const insideCwd = isInside(absolutePath, jailRoot);\n const allowRoots = policy.allowPaths ?? [];\n let insideAllow = false;\n\n if (!insideCwd) {\n for (const root of allowRoots) {\n const canonicalRoot = await canonicalize(root);\n\n if (isInside(absolutePath, canonicalRoot)) {\n insideAllow = true;\n\n break;\n }\n }\n }\n\n const spillDir = access === \"read\" && !insideCwd ? resolveSpillDir(policy) : undefined;\n\n if (spillDir !== undefined && isInside(absolutePath, await canonicalize(spillDir))) {\n return { absolutePath, relativePath: \"\" };\n }\n\n if (!insideCwd && !insideAllow) {\n throw new WorkspacePolicyError(`Path \"${inputPath}\" resolves outside the workspace jail.`, {\n type: \"path-escape\",\n path: inputPath,\n });\n }\n\n // `denyPaths` is evaluated relative to the jail root and wins even\n // when the path is comfortably inside `cwd`.\n const relativeToJail = insideCwd\n ? path.relative(jailRoot, absolutePath).split(path.sep).join(\"/\")\n : \"\";\n\n if (insideCwd && policy.denyPaths && policy.denyPaths.length > 0) {\n if (matchesAnyGlob(relativeToJail, policy.denyPaths)) {\n throw new WorkspacePolicyError(`Path \"${inputPath}\" is blocked by the workspace deny list.`, {\n type: \"path-escape\",\n path: inputPath,\n });\n }\n }\n\n return { absolutePath, relativePath: relativeToJail };\n}\n\n/**\n * Whether a command line is permitted by the policy's `shell` sub-policy —\n * `checkShellCommand(policy, command).allowed`.\n *\n * In argv mode (the default) the command must tokenize into a single argv\n * (no unquoted `;`, `&&`, `|`, backticks, `$`, redirection, …): the backend\n * spawns it with no shell, and unquoted metacharacters were exactly how an\n * injected command chain used to ride past the allowlist. In shell mode\n * (`interpreter: \"sh\"`) every simple command in the line is checked. Each\n * argv's executable basename is matched against `shell.deny` (wins, case\n * insensitive) then `shell.allow` (exhaustive when non-empty), then\n * `shell.denyPatterns` and `shell.allowCommand`. An absent `shell` block\n * means no command may run at all.\n *\n * Returns a plain `boolean`; use {@link checkShellCommand} for the reason\n * and the model-facing message. Redirection targets are checked lexically\n * here; `exec` re-checks them with symlinks resolved.\n *\n * @example\n * isCommandAllowed({ cwd, shell: { allow: [\"npm\"] } }, \"npm test\"); // true\n * isCommandAllowed({ cwd, shell: { allow: [\"npm\"] } }, \"rm -rf /\"); // false\n * isCommandAllowed({ cwd, shell: { allow: [\"npm\"] } }, \"npm test; rm -rf /\"); // false\n */\nexport function isCommandAllowed(policy: WorkspacePolicy, command: string): boolean {\n return checkShellCommand(policy, command).allowed;\n}\n\n/**\n * Build the exact environment a spawned process receives — `process.env`\n * is **never** inherited wholesale. The result is\n * `{ ...pick(process.env, inheritEnv), ...shell.env }`, so a command\n * cannot see `PATH` (and thus often cannot find `node`/`npm`) unless the\n * policy opts in via `shell.inheritEnv: [\"PATH\"]`. Explicit `shell.env`\n * values override inherited ones on key collision.\n *\n * @example\n * buildEnv({ cwd, shell: { inheritEnv: [\"PATH\"], env: { CI: \"1\" } } });\n * // → { PATH: <process PATH>, CI: \"1\" }\n */\nexport function buildEnv(policy: WorkspacePolicy): Record<string, string> {\n const shell = policy.shell;\n const env: Record<string, string> = {};\n\n if (!shell) {\n return env;\n }\n\n for (const key of shell.inheritEnv ?? []) {\n const value = process.env[key];\n\n if (value !== undefined) {\n env[key] = value;\n }\n }\n\n if (shell.env) {\n for (const [key, value] of Object.entries(shell.env)) {\n env[key] = value;\n }\n }\n\n return env;\n}\n\n/**\n * Variables removed from the environment of a shell-mode (`interpreter`\n * other than `\"none\"`) command: each changes how the shell itself starts, parses or\n * resolves the line (`ENV`/`BASH_ENV` run a startup file, `SHELLOPTS` +\n * `PS4` can run command substitutions, `BASH_FUNC_*` imports functions that\n * shadow commands, `CDPATH` moves `cd`, `IFS` re-splits words).\n */\nconst SHELL_STARTUP_VARIABLES = new Set([\n \"ENV\",\n \"BASH_ENV\",\n \"SHELLOPTS\",\n \"BASHOPTS\",\n \"CDPATH\",\n \"IFS\",\n \"PS4\",\n \"GLOBIGNORE\",\n]);\n\n/**\n * The environment a command actually receives: {@link buildEnv}, minus the\n * shell start-up variables when the policy runs lines through a shell.\n * Compared case-insensitively, because Windows environment names are.\n */\nexport function buildCommandEnv(policy: WorkspacePolicy): Record<string, string> {\n const env = buildEnv(policy);\n\n if ((policy.shell?.interpreter ?? \"none\") === \"none\") {\n return env;\n }\n\n for (const key of Object.keys(env)) {\n const upper = key.toUpperCase();\n\n if (SHELL_STARTUP_VARIABLES.has(upper) || upper.startsWith(\"BASH_FUNC_\")) {\n delete env[key];\n }\n }\n\n return env;\n}\n"],"mappings":"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"}
@@ -0,0 +1,275 @@
1
+ //#region ../../packages/ai-workspace/src/policy/powershell-aliases.ts
2
+ const FAMILIES = [
3
+ {
4
+ canonical: "rm",
5
+ names: [
6
+ "remove-item",
7
+ "ri",
8
+ "rm",
9
+ "del",
10
+ "erase",
11
+ "rd",
12
+ "rmdir"
13
+ ],
14
+ recurseFlag: "r",
15
+ forceFlag: "f",
16
+ params: [
17
+ ["recurse", "recurse"],
18
+ ["force", "force"],
19
+ ["path", "operand"],
20
+ ["literalpath", "operand"],
21
+ ["whatif", "switch"],
22
+ ["confirm", "switch"],
23
+ ["filter", "value"],
24
+ ["include", "value"],
25
+ ["exclude", "value"],
26
+ ["credential", "value"],
27
+ ["stream", "value"]
28
+ ]
29
+ },
30
+ {
31
+ canonical: "cp",
32
+ names: [
33
+ "copy-item",
34
+ "cpi",
35
+ "cp",
36
+ "copy"
37
+ ],
38
+ recurseFlag: "r",
39
+ forceFlag: "f",
40
+ params: [
41
+ ["recurse", "recurse"],
42
+ ["force", "force"],
43
+ ["path", "operand"],
44
+ ["literalpath", "operand"],
45
+ ["destination", "operand"],
46
+ ["whatif", "switch"],
47
+ ["confirm", "switch"],
48
+ ["filter", "value"],
49
+ ["include", "value"],
50
+ ["exclude", "value"],
51
+ ["credential", "value"]
52
+ ]
53
+ },
54
+ {
55
+ canonical: "mv",
56
+ names: [
57
+ "move-item",
58
+ "mi",
59
+ "mv",
60
+ "move"
61
+ ],
62
+ recurseFlag: "",
63
+ forceFlag: "f",
64
+ params: [
65
+ ["force", "force"],
66
+ ["path", "operand"],
67
+ ["literalpath", "operand"],
68
+ ["destination", "operand"],
69
+ ["whatif", "switch"],
70
+ ["confirm", "switch"],
71
+ ["filter", "value"],
72
+ ["include", "value"],
73
+ ["exclude", "value"],
74
+ ["credential", "value"]
75
+ ]
76
+ },
77
+ {
78
+ canonical: "ls",
79
+ names: [
80
+ "get-childitem",
81
+ "gci",
82
+ "ls",
83
+ "dir"
84
+ ],
85
+ recurseFlag: "R",
86
+ forceFlag: "a",
87
+ params: [
88
+ ["recurse", "recurse"],
89
+ ["force", "force"],
90
+ ["path", "operand"],
91
+ ["literalpath", "operand"],
92
+ ["filter", "value"],
93
+ ["include", "value"],
94
+ ["exclude", "value"],
95
+ ["depth", "value"],
96
+ ["name", "switch"]
97
+ ]
98
+ },
99
+ {
100
+ canonical: "cat",
101
+ names: [
102
+ "get-content",
103
+ "gc",
104
+ "cat",
105
+ "type"
106
+ ],
107
+ recurseFlag: "",
108
+ forceFlag: "",
109
+ params: [
110
+ ["path", "operand"],
111
+ ["literalpath", "operand"],
112
+ ["totalcount", "value"],
113
+ ["tail", "value"],
114
+ ["head", "value"],
115
+ ["encoding", "value"],
116
+ ["filter", "value"],
117
+ ["include", "value"],
118
+ ["exclude", "value"],
119
+ ["force", "switch"],
120
+ ["raw", "switch"],
121
+ ["wait", "switch"]
122
+ ]
123
+ },
124
+ {
125
+ canonical: "curl",
126
+ names: [
127
+ "invoke-webrequest",
128
+ "iwr",
129
+ "invoke-restmethod",
130
+ "irm",
131
+ "curl",
132
+ "wget"
133
+ ],
134
+ recurseFlag: "",
135
+ forceFlag: "",
136
+ params: [
137
+ ["uri", "operand"],
138
+ ["outfile", "outfile"],
139
+ ["method", "method"],
140
+ ["headers", "value"],
141
+ ["body", "value"],
142
+ ["contenttype", "value"],
143
+ ["useragent", "value"],
144
+ ["credential", "value"],
145
+ ["timeoutsec", "value"],
146
+ ["proxy", "value"]
147
+ ]
148
+ },
149
+ {
150
+ canonical: "kill",
151
+ names: [
152
+ "stop-process",
153
+ "spps",
154
+ "kill"
155
+ ],
156
+ recurseFlag: "",
157
+ forceFlag: "9",
158
+ params: [
159
+ ["id", "operand"],
160
+ ["name", "operand"],
161
+ ["force", "force"],
162
+ ["inputobject", "value"],
163
+ ["whatif", "switch"],
164
+ ["confirm", "switch"]
165
+ ]
166
+ }
167
+ ];
168
+ const BY_NAME = /* @__PURE__ */ new Map();
169
+ for (const family of FAMILIES) for (const name of family.names) BY_NAME.set(name, family);
170
+ /** PowerShell reads an en dash, em dash or horizontal bar as the parameter dash too. */
171
+ const FLAG_ORDER = "rRfa9";
172
+ const DASH = /^[-–—―]/;
173
+ /** Lowercase executable name used to look an alias up. */
174
+ function aliasKey(first) {
175
+ return (first.split(/[\\/]/).pop() ?? first).replace(/\.(exe|cmd|bat|com)$/i, "").toLowerCase();
176
+ }
177
+ /** Split `-Name` / `-Name:value` into the lowercase name and the inline value. */
178
+ function parameter(arg) {
179
+ if (!DASH.test(arg) || arg.length < 2) return null;
180
+ const body = arg.slice(1);
181
+ const colon = body.indexOf(":");
182
+ return colon === -1 ? { name: body.toLowerCase() } : {
183
+ name: body.slice(0, colon).toLowerCase(),
184
+ inline: body.slice(colon + 1)
185
+ };
186
+ }
187
+ /**
188
+ * The canonical argv of a PowerShell command that uses an alias or cmdlet
189
+ * name from the table (`Remove-Item -Recurse -Force x` → `["rm", "-rf", "x"]`),
190
+ * or `undefined` when the command has no canonical form.
191
+ */
192
+ function canonicalPowerShellArgv(argv) {
193
+ const family = BY_NAME.get(aliasKey(argv[0] ?? ""));
194
+ if (family === void 0) return;
195
+ let flags = "";
196
+ const operands = [];
197
+ const extras = [];
198
+ for (let index = 1; index < argv.length; index++) {
199
+ const arg = argv[index] ?? "";
200
+ const parsed = parameter(arg);
201
+ if (parsed === null) {
202
+ operands.push(arg);
203
+ continue;
204
+ }
205
+ const match = family.params.find(([name]) => parsed.name !== "" && name.startsWith(parsed.name));
206
+ if (match === void 0) continue;
207
+ const role = match[1];
208
+ if (role === "recurse") flags += family.recurseFlag;
209
+ else if (role === "force") flags += family.forceFlag;
210
+ else if (role === "switch") continue;
211
+ else {
212
+ const value = parsed.inline ?? argv[++index] ?? "";
213
+ if (role === "operand") operands.push(value);
214
+ else if (role === "outfile") extras.push("-o", value);
215
+ else if (role === "method") extras.push("-X", value);
216
+ }
217
+ }
218
+ const unique = [...FLAG_ORDER].filter((flag) => flags.includes(flag)).join("");
219
+ return [
220
+ family.canonical,
221
+ ...unique !== "" ? [`-${unique}`] : [],
222
+ ...extras,
223
+ ...operands
224
+ ];
225
+ }
226
+ /** The names PowerShell's `cd` goes by. */
227
+ const POWERSHELL_CD_NAMES = /* @__PURE__ */ new Set([
228
+ "cd",
229
+ "sl",
230
+ "set-location",
231
+ "chdir"
232
+ ]);
233
+ /** Directory stack commands, refused because the policy does not track them. */
234
+ const POWERSHELL_DIRECTORY_STACK = /* @__PURE__ */ new Set([
235
+ "push-location",
236
+ "pushd",
237
+ "pop-location",
238
+ "popd"
239
+ ]);
240
+ /**
241
+ * Normalise a PowerShell `cd` to `[cd, directory]`: `-Path x`, `-LiteralPath x`
242
+ * (and their prefixes, `-Path:x`) become the directory operand. Returns `null`
243
+ * for any other parameter (`-PassThru`, `-StackName`, …).
244
+ */
245
+ function normalizePowerShellCd(words) {
246
+ const first = words[0];
247
+ if (first === void 0) return null;
248
+ const operands = [];
249
+ for (let index = 1; index < words.length; index++) {
250
+ const word = words[index];
251
+ const parsed = word.quoted ? null : parameter(word.value);
252
+ if (parsed === null) {
253
+ operands.push(word);
254
+ continue;
255
+ }
256
+ const isPath = parsed.name.length >= 2 && "path".startsWith(parsed.name);
257
+ const isLiteral = parsed.name.length >= 1 && "literalpath".startsWith(parsed.name);
258
+ if (!isPath && !isLiteral) return null;
259
+ if (parsed.inline !== void 0) operands.push({
260
+ ...word,
261
+ raw: parsed.inline,
262
+ value: parsed.inline
263
+ });
264
+ else {
265
+ const next = words[++index];
266
+ if (next === void 0) return null;
267
+ operands.push(next);
268
+ }
269
+ }
270
+ return [first, ...operands];
271
+ }
272
+
273
+ //#endregion
274
+ export { POWERSHELL_CD_NAMES, POWERSHELL_DIRECTORY_STACK, canonicalPowerShellArgv, normalizePowerShellCd };
275
+ //# sourceMappingURL=powershell-aliases.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"powershell-aliases.mjs","names":[],"sources":["../../../../../../../../packages/ai-workspace/src/policy/powershell-aliases.ts"],"sourcesContent":["import type { ShellWord } from \"./shell-parser\";\n\n/**\n * PowerShell spells the same operation many ways (`Remove-Item`, `ri`, `rm`,\n * `del`, `erase`, `rd`, `rmdir`; parameters can be shortened to any\n * unambiguous prefix). Deny rules are written against one spelling, so the\n * policy also checks each command in its canonical POSIX-style form:\n * `Remove-Item -Recurse -Force x` is checked as `rm -rf x`.\n */\n\ntype ParamRole = \"recurse\" | \"force\" | \"operand\" | \"value\" | \"switch\" | \"method\" | \"outfile\";\n\ninterface Family {\n /** The canonical executable name. */\n canonical: string;\n /** Every spelling (lowercase) that means this family. */\n names: string[];\n /** Flag characters emitted for `-Recurse` / `-Force` (empty: not emitted). */\n recurseFlag: string;\n forceFlag: string;\n /** Parameters in priority order (the first whose name starts with the typed prefix wins). */\n params: Array<[string, ParamRole]>;\n}\n\nconst FAMILIES: Family[] = [\n {\n canonical: \"rm\",\n names: [\"remove-item\", \"ri\", \"rm\", \"del\", \"erase\", \"rd\", \"rmdir\"],\n recurseFlag: \"r\",\n forceFlag: \"f\",\n params: [\n [\"recurse\", \"recurse\"],\n [\"force\", \"force\"],\n [\"path\", \"operand\"],\n [\"literalpath\", \"operand\"],\n [\"whatif\", \"switch\"],\n [\"confirm\", \"switch\"],\n [\"filter\", \"value\"],\n [\"include\", \"value\"],\n [\"exclude\", \"value\"],\n [\"credential\", \"value\"],\n [\"stream\", \"value\"],\n ],\n },\n {\n canonical: \"cp\",\n names: [\"copy-item\", \"cpi\", \"cp\", \"copy\"],\n recurseFlag: \"r\",\n forceFlag: \"f\",\n params: [\n [\"recurse\", \"recurse\"],\n [\"force\", \"force\"],\n [\"path\", \"operand\"],\n [\"literalpath\", \"operand\"],\n [\"destination\", \"operand\"],\n [\"whatif\", \"switch\"],\n [\"confirm\", \"switch\"],\n [\"filter\", \"value\"],\n [\"include\", \"value\"],\n [\"exclude\", \"value\"],\n [\"credential\", \"value\"],\n ],\n },\n {\n canonical: \"mv\",\n names: [\"move-item\", \"mi\", \"mv\", \"move\"],\n recurseFlag: \"\",\n forceFlag: \"f\",\n params: [\n [\"force\", \"force\"],\n [\"path\", \"operand\"],\n [\"literalpath\", \"operand\"],\n [\"destination\", \"operand\"],\n [\"whatif\", \"switch\"],\n [\"confirm\", \"switch\"],\n [\"filter\", \"value\"],\n [\"include\", \"value\"],\n [\"exclude\", \"value\"],\n [\"credential\", \"value\"],\n ],\n },\n {\n canonical: \"ls\",\n names: [\"get-childitem\", \"gci\", \"ls\", \"dir\"],\n recurseFlag: \"R\",\n forceFlag: \"a\",\n params: [\n [\"recurse\", \"recurse\"],\n [\"force\", \"force\"],\n [\"path\", \"operand\"],\n [\"literalpath\", \"operand\"],\n [\"filter\", \"value\"],\n [\"include\", \"value\"],\n [\"exclude\", \"value\"],\n [\"depth\", \"value\"],\n [\"name\", \"switch\"],\n ],\n },\n {\n canonical: \"cat\",\n names: [\"get-content\", \"gc\", \"cat\", \"type\"],\n recurseFlag: \"\",\n forceFlag: \"\",\n params: [\n [\"path\", \"operand\"],\n [\"literalpath\", \"operand\"],\n [\"totalcount\", \"value\"],\n [\"tail\", \"value\"],\n [\"head\", \"value\"],\n [\"encoding\", \"value\"],\n [\"filter\", \"value\"],\n [\"include\", \"value\"],\n [\"exclude\", \"value\"],\n [\"force\", \"switch\"],\n [\"raw\", \"switch\"],\n [\"wait\", \"switch\"],\n ],\n },\n {\n canonical: \"curl\",\n names: [\"invoke-webrequest\", \"iwr\", \"invoke-restmethod\", \"irm\", \"curl\", \"wget\"],\n recurseFlag: \"\",\n forceFlag: \"\",\n params: [\n [\"uri\", \"operand\"],\n [\"outfile\", \"outfile\"],\n [\"method\", \"method\"],\n [\"headers\", \"value\"],\n [\"body\", \"value\"],\n [\"contenttype\", \"value\"],\n [\"useragent\", \"value\"],\n [\"credential\", \"value\"],\n [\"timeoutsec\", \"value\"],\n [\"proxy\", \"value\"],\n ],\n },\n {\n canonical: \"kill\",\n names: [\"stop-process\", \"spps\", \"kill\"],\n recurseFlag: \"\",\n forceFlag: \"9\",\n params: [\n [\"id\", \"operand\"],\n [\"name\", \"operand\"],\n [\"force\", \"force\"],\n [\"inputobject\", \"value\"],\n [\"whatif\", \"switch\"],\n [\"confirm\", \"switch\"],\n ],\n },\n];\n\nconst BY_NAME = new Map<string, Family>();\n\nfor (const family of FAMILIES) {\n for (const name of family.names) {\n BY_NAME.set(name, family);\n }\n}\n\n/** PowerShell reads an en dash, em dash or horizontal bar as the parameter dash too. */\nconst FLAG_ORDER = \"rRfa9\";\n\nconst DASH = /^[-–—―]/;\n\n/** Lowercase executable name used to look an alias up. */\nfunction aliasKey(first: string): string {\n const base = first.split(/[\\\\/]/).pop() ?? first;\n\n return base.replace(/\\.(exe|cmd|bat|com)$/i, \"\").toLowerCase();\n}\n\n/** Split `-Name` / `-Name:value` into the lowercase name and the inline value. */\nfunction parameter(arg: string): { name: string; inline?: string } | null {\n if (!DASH.test(arg) || arg.length < 2) {\n return null;\n }\n\n const body = arg.slice(1);\n const colon = body.indexOf(\":\");\n\n return colon === -1\n ? { name: body.toLowerCase() }\n : { name: body.slice(0, colon).toLowerCase(), inline: body.slice(colon + 1) };\n}\n\n/**\n * The canonical argv of a PowerShell command that uses an alias or cmdlet\n * name from the table (`Remove-Item -Recurse -Force x` → `[\"rm\", \"-rf\", \"x\"]`),\n * or `undefined` when the command has no canonical form.\n */\nexport function canonicalPowerShellArgv(argv: readonly string[]): string[] | undefined {\n const family = BY_NAME.get(aliasKey(argv[0] ?? \"\"));\n\n if (family === undefined) {\n return undefined;\n }\n\n let flags = \"\";\n const operands: string[] = [];\n const extras: string[] = [];\n\n for (let index = 1; index < argv.length; index++) {\n const arg = argv[index] ?? \"\";\n const parsed = parameter(arg);\n\n if (parsed === null) {\n operands.push(arg);\n\n continue;\n }\n\n const match = family.params.find(\n ([name]) => parsed.name !== \"\" && name.startsWith(parsed.name),\n );\n\n if (match === undefined) {\n continue;\n }\n\n const role = match[1];\n\n if (role === \"recurse\") {\n flags += family.recurseFlag;\n } else if (role === \"force\") {\n flags += family.forceFlag;\n } else if (role === \"switch\") {\n continue;\n } else {\n const value = parsed.inline ?? argv[++index] ?? \"\";\n\n if (role === \"operand\") {\n operands.push(value);\n } else if (role === \"outfile\") {\n extras.push(\"-o\", value);\n } else if (role === \"method\") {\n extras.push(\"-X\", value);\n }\n }\n }\n\n // Keep each flag once, in a stable order.\n const unique = [...FLAG_ORDER].filter((flag) => flags.includes(flag)).join(\"\");\n\n return [family.canonical, ...(unique !== \"\" ? [`-${unique}`] : []), ...extras, ...operands];\n}\n\n/** The names PowerShell's `cd` goes by. */\nexport const POWERSHELL_CD_NAMES = new Set([\"cd\", \"sl\", \"set-location\", \"chdir\"]);\n\n/** Directory stack commands, refused because the policy does not track them. */\nexport const POWERSHELL_DIRECTORY_STACK = new Set([\n \"push-location\",\n \"pushd\",\n \"pop-location\",\n \"popd\",\n]);\n\n/**\n * Normalise a PowerShell `cd` to `[cd, directory]`: `-Path x`, `-LiteralPath x`\n * (and their prefixes, `-Path:x`) become the directory operand. Returns `null`\n * for any other parameter (`-PassThru`, `-StackName`, …).\n */\nexport function normalizePowerShellCd(words: ShellWord[]): ShellWord[] | null {\n const first = words[0];\n\n if (first === undefined) {\n return null;\n }\n\n const operands: ShellWord[] = [];\n\n for (let index = 1; index < words.length; index++) {\n const word = words[index] as ShellWord;\n const parsed = word.quoted ? null : parameter(word.value);\n\n if (parsed === null) {\n operands.push(word);\n\n continue;\n }\n\n const isPath = parsed.name.length >= 2 && \"path\".startsWith(parsed.name);\n const isLiteral = parsed.name.length >= 1 && \"literalpath\".startsWith(parsed.name);\n\n if (!isPath && !isLiteral) {\n return null;\n }\n\n if (parsed.inline !== undefined) {\n operands.push({ ...word, raw: parsed.inline, value: parsed.inline });\n } else {\n const next = words[++index];\n\n if (next === undefined) {\n return null;\n }\n\n operands.push(next);\n }\n }\n\n return [first, ...operands];\n}\n"],"mappings":"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"}