Skip to content
Octopus Research Institute
EcosystemApache-2.0TypeScript

octopus-inspect

A static governance linter for AI agent workflows — catch governance gaps before production does.

A static governance linter for AI agent workflows — catch governance gaps before production does.

Our research relates to this project; it is ecosystem engineering, not an Institute research output unless marked otherwise.

Readme

Mirrored from the project's README on GitHub.

English | 简体中文

Inspect

Catch governance holes before production does. A static governance linter for AI agent workspaces — and a rule host: an engine that walks a workspace, runs governance rules over it, and produces deterministic findings.

Part of Octopus Core — the open infrastructure stack for governed AI. One job per repo, along the agent lifecycle: Scout · Observe · Experience · Blackboard · Workstate · Runtime · Replay — with Inspect governing every stage. The whole stack rides one root primitive: Evidence — the canonical, tamper-evident atom, the root category the rest of Octopus is built on.

This repo — Inspect · Govern (every stage): Governance lint for AI workspaces.

Workspace → Rules → Findings → Report (pretty / json / sarif)

Point Inspect at a directory of AI-agent configuration — prompts, agent and workflow definitions, MCP manifests, decision records — and it flags the governance holes that don't show up until something ships: a committed secret, an untrusted variable spliced into a prompt, an auto-executing action with no approval gate, a wildcard tool grant, a decision recorded with no evidence, a workflow that can never complete, a dependency pinned to a mutable tag. Findings come back sorted and deterministic, ready for a human, a JSON pipeline, or CI code scanning.

Boundaries

Inspect is static and self-contained. It reads the tree on disk and reasons about the shape of what it finds. It does not execute your workspace, import a runtime, call a network, reproduce another system's policy semantics, or modify any file. Every built-in rule is pure: same bytes on disk → same findings, in the same order.

That last boundary is deliberate. A check that needs to understand a specific runtime's semantics — what a given policy engine actually permits, what a blackboard counts as a valid handoff — does not belong in the built-in rules; baking it in would fork the definition of "safe". Such checks arrive as plugins contributed by the runtime that owns those semantics, so there is a single source of truth for what safe means. The built-ins only flag the shape that most often hides a hole.

It is built on the first-party octopus-evidence primitive — its only runtime dependency, and the same Evidence atom the rest of the stack rides. That single dependency is what lets --format evidence turn each finding into a tamper-evident, independently-verifiable unit: your linter's findings are court-admissible without trusting the linter. The repo is otherwise self-contained and takes on no third-party dependencies.

Install & build

npm install
npm run typecheck   # tsc --noEmit
npm test            # node --test
npm run build       # emit dist/
npm run example     # run the bundled demo workspace

Requires Node ≥ 22. Inspect scans JSON / JSONC / text / Markdown files; .mcp.json and .prompt are recognized. YAML is a documented future extension and is not parsed yet (see Boundaries and docs/DESIGN.md).

CLI

octopus-inspect [path]                 # inspect a directory or file (default ".")
octopus-inspect . --format sarif       # emit SARIF for CI code scanning
octopus-inspect . --format evidence    # emit tamper-evident Evidence per finding
octopus-inspect . --threshold warning  # fail the build on warnings too
OptionMeaning
--format <f>Output format: pretty | json | sarif | evidence (default pretty)
--config <file>Config file to use (default .octoinspect.json at the root)
--threshold <s>Severity that fails the run: error | warning | info (default error)
--no-colorDisable ANSI color in pretty output
--versionPrint version and exit
--helpShow help

Exit codes: 0 clean · 1 findings at or above the threshold · 2 configuration error (bad config, an unloadable plugin, a colliding rule id, or a missing path). Pretty output is colorized only on a TTY; --no-color forces it off.

GitHub code scanning (primary path)

SARIF is the standard static-analysis exchange format, and it is Inspect's primary distribution path: emit SARIF in CI and GitHub renders every finding inline on the PR, in the Security tab, and as a check. Add a step to your workflow:

# .github/workflows/inspect.yml
name: inspect
on: [push, pull_request]

jobs:
  governance:
    runs-on: ubuntu-latest
    permissions:
      security-events: write   # required to upload SARIF
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: "22"
      - run: npx octopus-inspect . --format sarif > inspect.sarif
      - uses: github/codeql-action/upload-sarif@v3
        with:
          sarif_file: inspect.sarif

The SARIF run carries each rule's id, title, description, and default level, so findings land already labeled and grouped. To also fail the job on findings, run a second octopus-inspect . (pretty or json) with your chosen --threshold; the upload step above never fails the build on its own.

GitHub Action (drop-in)

A composite action wraps the SARIF path above so any repo can adopt it in two steps: run Inspect, then upload the SARIF. The action runs the published npm package via npx — no build, no checkout of this repo. The SARIF upload is kept out of the action so permissions stay in the caller's workflow (the security-events: write grant belongs to you, not to a third-party action).

# .github/workflows/inspect.yml
name: inspect
on: [push, pull_request]

permissions:
  security-events: write # required to upload SARIF
  contents: read

jobs:
  governance:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - id: inspect
        uses: octoryn/octopus-inspect@v0.3.2
        with:
          path: .
          fail-on-findings: "false" # report without failing the build
      - uses: github/codeql-action/upload-sarif@v3
        with:
          sarif_file: ${{ steps.inspect.outputs.sarif-file }}

Findings then render inline on the PR, in the Security tab, and as a check. Set fail-on-findings: "true" (the default) to also fail the step when Inspect exits nonzero — combine that with args: "--threshold warning" to choose what severity blocks the build.

Inputs

InputDefaultMeaning
path.Workspace directory or file to scan.
args""Extra CLI arguments passed through to octopus-inspect (e.g. --threshold warning).
version0.3.2npm version/spec of octopus-inspect to run (octopus-inspect@<version>).
sarif-fileoctopus-inspect.sarifPath the SARIF report is written to.
fail-on-findingstrueWhen true, a nonzero Inspect exit (findings at/above the threshold, or a config error) fails the step; when false, the step always succeeds so the SARIF can still be uploaded.

Outputs

OutputMeaning
sarif-filePath to the SARIF report the action produced — feed it to upload-sarif.

The CLI writes SARIF to stdout; the action redirects that to sarif-file and captures the exit code so fail-on-findings can gate the step independently of the upload.

Programmatic API

Everything the CLI does is available as a library:

import { inspect, shouldFail, formatSarif } from "octopus-inspect";

const report = await inspect("./workspace");

for (const f of report.findings) {
  console.log(`${f.severity}\t${f.file}:${f.line ?? 0}\t${f.ruleId}\t${f.message}`);
}

console.log(report.summary);              // { error, warning, info }
console.log(shouldFail(report, "error")); // would CI fail?

const sarif = formatSarif(report);        // SARIF 2.1.0 string

inspect(root, options?) returns a deterministic InspectReport ({ root, findings, fileCount, ruleCount, summary }); pass { config, rules } to override either. formatPretty and formatJson are the other two reporters. The full built-in rule set is exported as builtinRules.

Built-in rules

Seven static rules ship in the box. Each has a stable id and a default severity you can override or disable in config.

IdDefaultKindWhat it flags
secret-in-sourceerrortextHardcoded credentials — cloud keys, provider API keys, private-key blocks, generic secret = "…" assignments — committed to the tree. Placeholders (your-…, ${…}, process.env) stay quiet; the matched secret is redacted in the finding.
prompt-injection-sinkwarningtext/jsonA prompt or instruction template that splices an untrusted variable ({{ user_input }}, ${userMessage}, %(query)s) straight into the instructions, where it could smuggle in adversarial instructions.
unsafe-autonomyerrorstructuralA config object that marks itself auto-executing (autonomy, autoApprove, requireApproval: false, …) next to a side-effect marker (execute, command, tool, …) while declaring no approval or policy field.
overbroad-permissionwarningstructuralA wildcard (*, all, *:*) in an allowlist, tool list, or scope grant — every capability allowed instead of a least-privilege set.
missing-evidencewarningstructuralA record stating a decision / claim / conclusion that carries no evidence / source / rationale / citation field. Shape only — it does not judge whether the evidence is good, only that some is present.
circular-workflowerrorstructuralA dependency cycle among workflow/agent steps (via next / then / dependsOn / requires / needs edges) parsed from JSON. A cyclic graph can never complete.
unpinned-agent-dependencyinfostructuralAn MCP/agent launch arg (npx pkg@latest) or manifest dependency pinned to a mutable tag (latest, *, next, …), where the executed code can change without review.

"Structural" rules read the parsed shape of JSON/JSONC objects; they never reason about a runtime's semantics — that boundary is what keeps "safe" defined in one place. secret-in-source and prompt-injection-sink also scan plain text and prompt files.

OWASP Agentic Top 10 mapping

Every built-in rule is tagged with the category it addresses in the OWASP Top 10 for Agentic Applications (2026) — surfaced in SARIF (properties.tags, e.g. OWASP-ASI-2026:ASI01) so findings slot into the taxonomy security teams already use.

RuleOWASP Agentic 2026
secret-in-sourceASI03 · Identity and Privilege Abuse
prompt-injection-sinkASI01 · Agent Goal Hijack
unsafe-autonomyASI02 · Tool Misuse · ASI09 · Human-Agent Trust
overbroad-permissionASI03 · Identity and Privilege Abuse
missing-evidenceASI09 · Human-Agent Trust Exploitation
circular-workflowASI08 · Cascading Failures
unpinned-agent-dependencyASI04 · Agentic Supply Chain Vulnerabilities

Inspect's static rules cover the subset of the Top 10 visible in committed workspace config; runtime-only risks are out of a linter's scope by design. The catalog is exported (OWASP_AGENTIC_2026, owaspLabel) for use in plugins.

Configuration

Inspect looks for .octoinspect.json (JSONC — comments allowed) at the root, or takes an explicit --config <file>. A malformed or absent config is not an error; the run proceeds with defaults.

{
  // Extra ignore globs, added to the built-in defaults (node_modules, .git, dist, …).
  "ignore": ["fixtures/**", "**/*.generated.json"],

  // Per-rule overrides: "off" disables a rule; a severity re-grades it.
  "rules": {
    "unpinned-agent-dependency": "off",     // don't care in this repo
    "prompt-injection-sink": "error",       // treat as blocking here
    "missing-evidence": "info"              // downgrade to advisory
  },

  // Plugin module specifiers, resolved from the workspace root.
  "plugins": ["./inspect-plugins/runtime-policy.js"],

  // Skip files larger than this many bytes (default 1 MiB).
  "maxFileBytes": 524288
}

A rule set to "off" is not run at all. A severity override (or a plugin rule's own default) wins over the built-in default; that effective severity is what the threshold and the exit code compare against. See .octoinspect.json for a fully commented example.

Plugins

Inspect is a rule host. When a check needs a specific runtime's semantics, that runtime ships the rule as a plugin and Inspect hosts it — so the definition of "safe" is never forked into the built-ins. A plugin is just a module that exports a { name, rules } object (as the default export, a named plugin, or the module shape itself):

// inspect-plugins/runtime-policy.ts
import { definePlugin, type Rule, type Workspace } from "octopus-inspect";

const noProdWrites: Rule = {
  id: "runtime/no-prod-writes",
  title: "Agent may write to production without a policy",
  description: "Rejects a manifest that grants prod write scope with no attached policy.",
  severity: "error",
  check(workspace: Workspace) {
    // Read the shape of the workspace; return raw findings.
    return [];
  },
};

export default definePlugin({ name: "runtime-policy", rules: [noProdWrites] });

Reference it from config and its rules run alongside the built-ins:

{ "plugins": ["./inspect-plugins/runtime-policy.js"] }

Relative specifiers resolve against the workspace root; bare specifiers resolve as normal node modules. Duplicate rule ids are rejected (a plugin can never silently shadow another rule), and a plugin that fails to load is reported as a configuration error rather than aborting the run. The parseJsonc, walkJson, isJsonObject, and findKeyLine helpers are exported to make structural plugin rules easy to author.

A complete, working reference plugin lives at inspect-plugins/runtime-policy.ts: a semantic rule that derives its verdict from octopus-runtime's real autonomy model rather than re-encoding it. See docs/PLUGINS.md for the walkthrough — this is the pattern to copy for a semantic check a static linter can't do alone.

Design

The authoritative architecture and contracts live in docs/DESIGN.md — the rule-host philosophy, the Workspace → Rules → Findings → Report pipeline, the determinism guarantees, and the config/plugin model. Read it before making changes; code is written against that spec.

License

Apache-2.0 © Octoryn.

Topics

  • linter
  • governance
  • governed-ai
  • static-analysis

Related research areas

  • Governed AI Systems
  • Responsible AI
All open source