{"record":{"id":"732f792262d13e71","repo":"ruvnet/ruflo","slug":"policy-administration-requires-an-interactive-loca","errorCode":null,"errorMessage":"policy administration requires an interactive local terminal","messagePattern":"policy administration requires an interactive local terminal","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"v3/@claude-flow/cli/src/commands/policy.ts","lineNumber":33,"sourceCode":"  setPolicyMode,\n  upsertPolicyRule,\n  verifyPolicyLedger,\n} from '../services/policy-runtime.js';\n\nfunction argJson<T>(value: string | undefined, label: string): T {\n  if (!value) throw new Error(`${label} requires a JSON argument`);\n  try { return JSON.parse(value) as T; }\n  catch { throw new Error(`${label} must be valid JSON`); }\n}\n\nfunction print(data: unknown): CommandResult {\n  output.writeln(JSON.stringify(data, null, 2));\n  return { success: true, exitCode: 0, data };\n}\n\nfunction requireInteractiveAdministrator(): void {\n  if (!process.stdin.isTTY || !process.stdout.isTTY) {\n    throw new Error('policy administration requires an interactive local terminal');\n  }\n}\n\nexport const policyCommand: Command = {\n  name: 'policy',\n  description: 'Agentic policy engine — evaluate actions, manage rules/approvals, and verify the decision ledger (ADR-324)',\n  options: [\n    { name: 'mode', type: 'string', description: 'Policy mode: legacy | observe | enforce' },\n    { name: 'project-root', type: 'string', description: 'Project root containing .claude-flow/policy' },\n  ],\n  async action(context: CommandContext): Promise<CommandResult> {\n    const args = (context as { args?: string[] }).args ?? [];\n    const flags = (context as { flags?: Record<string, unknown> }).flags ?? {};\n    const root = String(flags.projectRoot ?? process.cwd());\n    const operation = args[0] ?? 'status';\n    try {\n      if (operation === 'init' || operation === 'migrate') {\n        if (flags.mode || args[1]) requireInteractiveAdministrator();","sourceCodeStart":15,"sourceCodeEnd":51,"githubUrl":"https://github.com/ruvnet/ruflo/blob/fa13ee4ad60ac2090b1480656eb233521790d640/v3/@claude-flow/cli/src/commands/policy.ts#L15-L51","documentation":"requireInteractiveAdministrator() throws when stdin or stdout is not a TTY and the requested policy operation mutates state (init/migrate with a --mode or positional mode, rule add, budget set, revoke). Policy administration is deliberately gated to a local interactive terminal so CI jobs, piped stdin, and daemons cannot silently rewrite policy rules or budgets.","triggerScenarios":"Running `ruflo policy init --mode enforce` in CI, under nohup, from a Docker exec without -t, or piping input: echo ... | ruflo policy rule add '{...}'. Also triggered when only one of stdin/stdout is redirected.","commonSituations":"Trying to automate policy setup in Dockerfiles or CI pipelines; running through ssh with redirected stdio; testing via a shell script with stdin from a file.","solutions":["Run the mutating command in a real interactive terminal (local shell or interactive ssh)","For scripted environments, mutate the policy state via the policy-runtime library API instead of the CLI","Note plain `ruflo policy init` without --mode does NOT require a TTY — only mode changes and rule/budget/revoke mutations do","As a last resort wrap in a pty (script -qec 'ruflo policy init --mode enforce' /dev/null), but prefer the API route"],"exampleFix":"# before (CI, non-TTY)\nruflo policy init --mode enforce\n\n# after (plain migrate is allowed non-interactively)\nruflo policy init   # no --mode; set the mode later from an interactive terminal","handlingStrategy":"validation","validationCode":"const isInteractive = Boolean(process.stdin.isTTY && process.stdout.isTTY);\nconst needsTty = ['init-with-mode', 'rule add', 'budget set', 'revoke'];\nif (needsTty.includes(op) && !isInteractive) {\n  // fall back to the policy-runtime library API instead of the CLI\n}","typeGuard":"function isInteractiveTerminal(): boolean {\n  return process.stdin.isTTY === true && process.stdout.isTTY === true;\n}","tryCatchPattern":"try {\n  await runPolicyCli(['init', '--mode', mode]);\n} catch (err) {\n  if (err instanceof Error && err.message.includes('interactive local terminal')) {\n    // surface to the user to run manually, or use the library API from a TTY session\n  } else throw err;\n}","preventionTips":["Design CI pipelines to never mutate policy state via the CLI","Split automation into non-TTY-safe reads (status/audit/verify) and interactive writes","Use docker exec -it / script -qec when a human must run mutations through a wrapper"],"tags":["cli","policy","tty","ci","security"],"backgroundTag":"non-interactive-terminal","analyzedSha":"fa13ee4ad60ac2090b1480656eb233521790d640","analyzedAt":"2026-08-18T21:34:22.708Z","contentChangedAt":"2026-08-18T21:34:22.708Z","schemaVersion":2},"datasetVersion":"2026-09-23T08:17:48.524Z"}