Skip to content

feat(scaffolde): register native projected capabilities - #49

Merged
pai-scaffolde merged 3 commits into
mainfrom
feat/scaffolde-capability-runtime
Jul 27, 2026
Merged

feat(scaffolde): register native projected capabilities#49
pai-scaffolde merged 3 commits into
mainfrom
feat/scaffolde-capability-runtime

Conversation

@pai-scaffolde

@pai-scaffolde pai-scaffolde commented Jul 26, 2026

Copy link
Copy Markdown
Collaborator

Summary

  • register scaffolde_capability as a native Hermes tool
  • validate projected descriptors, versions, authority, arguments, and managed deployment hashes
  • require payload-scoped approval for write operations with exact large-payload previews
  • fail closed on interpreter flags, registry/entrypoint tampering, timeouts, execution failures, and nonzero exits
  • expose explicit absent/degraded semantics without silently falling back to generic providers

Security model

  • registry and entrypoint SHA-256 values must match $HERMES_HOME/.scaffolde/deployment.json
  • command is fixed to bun with exactly one managed .ts entrypoint and no interpreter flags
  • subprocess execution is argv-only with shell=False
  • inherited environment is descriptor-allowlisted and secret-like output is bounded/redacted
  • every risk: write operation uses payload-scoped approval; large payloads use an exact temporary 0600 preview plus SHA-256

Verification

  • affected Hermes suites — 569 passed
  • ruff check on changed Python files — passed
  • independent adversarial re-review — PASS
  • strict cross-repo projection loaded the Scaffolde registry and invoked the real Gmail read operation successfully
  • live read-only sent-mail regression returned zero Sarah Guo matches

Deployment order

Land this consumer before Scaffolde producer PR pai-scaffolde/scaffolde-ai#2086. Without a projected registry, the tool reports an explicit absent/degraded status.

Adds a narrow-waist `scaffolde_capability` tool that reads a
Scaffolde-authored descriptor registry at
`$HERMES_HOME/scaffolde/capabilities.json` and executes declared
operations as argv-only subprocesses (no shell).

- tools/scaffolde_capabilities.py: registry loader + validator +
  invoker. Fail-closed on schema, non-bun entrypoints, entrypoints
  outside HERMES_HOME, entrypoints/registry that do not match their
  Scaffolde-managed sha256 in .scaffolde/deployment.json,
  HOME/PATH overrides, secret-looking descriptor values, and
  malformed argv templates. Env is allowlist-only (inherit + set),
  stdout/stderr are truncated and redacted, write-risk operations are
  approval-gated with a 0600 temp preview for oversized payloads.
- tools/scaffolde_capability_tool.py: status/list/invoke tool schema
  and handler, registered into the tool registry.
- agent/scaffolde_capability_prompt.py + agent/system_prompt.py:
  routing block emitted only when the tool is live and the registry is
  valid, ahead of skill guidance.
- toolsets.py: add `scaffolde_capability` to the Hermes core toolset.
- tests/tools/test_scaffolde_capability_tool.py: registry status
  matrix, path/secret/command rejection, argument validation and
  no-shell argv, env allowlist, approval gating, degraded semantics.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 091c08525f

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

else:
script = Path(_expand_home(args[0], hermes_home))
if not script.is_absolute():
script = hermes_home / script

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Resolve entrypoints against their execution cwd

When a descriptor uses an allowed relative entrypoint such as runner.ts with a non-root cwd, validation hashes $HERMES_HOME/runner.ts here, but execution passes runner.ts unchanged while setting the subprocess cwd, so Bun executes <cwd>/runner.ts instead. Bun 1.2.14's --help confirms that cwd is used to resolve files and entrypoints. This lets an unmanaged script execute under a registry that was reported valid; resolve and hash the relative entrypoint against the declared cwd, then execute that same resolved path.

Useful? React with 👍 / 👎.

Comment on lines +446 to +448
proc = subprocess.run(
argv,
cwd=desc["entrypoint"]["cwd"],

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Revalidate the entrypoint after approval

If a write invocation waits for interactive approval and the managed entrypoint is replaced during that wait, the hash checked at the start of invoke_capability is stale by the time this call runs, so the replacement bytes execute without ever matching deployment metadata. The same race exists with a smaller window for reads. Pin the verified file or repeat integrity validation immediately before execution so approval cannot create a large provenance-bypass window.

Useful? React with 👍 / 👎.

Comment on lines +25 to +27
_SECRET_VALUE_RE = re.compile(
r"(-----BEGIN [A-Z ]*PRIVATE KEY-----|xox[baprs]-|gh[pousr]_[A-Za-z0-9_]+|sk-[A-Za-z0-9_-]{12,}|AIza[0-9A-Za-z_-]{20,}|ya29\.)"
)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Redact complete prefixed credentials

When a capability prints a Slack or Google OAuth credential, these alternatives match only xoxb- or ya29.; _redact replaces only that prefix and returns the entire credential payload in stdout/stderr to the model and persisted conversation. For example, xoxb-1234567890-abcdefghijklmnop becomes [REDACTED]1234567890-abcdefghijklmnop. Extend each pattern through the complete token, or use the repository's established sensitive-text redactor.

Useful? React with 👍 / 👎.

Comment on lines +380 to +381
if isinstance(value, str) and len(value) > 160:
preview[name] = f"{value[:160]}… ({len(value)} chars)"

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Show the complete payload before approval

When an individual write argument exceeds 160 characters but the canonical payload remains at most 2,000 characters—for example, a typical email body—the approval reason contains only its first 160 characters and no review file. Prompt-injected or simply unintended content can therefore be hidden in the omitted suffix while the user is asked to authorize the send. Use the exact-preview-file path whenever any field is truncated, or display the complete inline payload.

Useful? React with 👍 / 👎.

Comment thread toolsets.py
Comment on lines +62 to +63
# Scaffolde native capability narrow waist (status/list/invoke over descriptor registry)
"scaffolde_capability",

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Gate the capability tool on a valid registry

For every installation without a valid Scaffolde deployment, placing this tool in _HERMES_CORE_TOOLS without a check_fn sends an unusable schema on every model request; the new test explicitly locks in that behavior even when the registry is absent. Register it with the existing registry_has_valid_capabilities check (and expose registry diagnostics outside the model schema if needed) so non-Scaffolde users pay zero permanent tool footprint.

AGENTS.md reference: AGENTS.md:L193-L195

Useful? React with 👍 / 👎.

Comment on lines +451 to +453
text=True,
capture_output=True,
timeout=max(1, min(int(timeout), 300)),

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Enforce output limits while reading

When a capability emits a large or unbounded stream, capture_output=True buffers all stdout and stderr in memory before the later 64K/16K slices are applied. A runaway or data-heavy runner can therefore exhaust the long-lived CLI or gateway process despite the apparent output caps. Read through bounded pipes or temporary files and terminate or drain once the configured limits are reached.

Useful? React with 👍 / 👎.

Comment on lines +115 to +119
try:
actual = hashlib.sha256((hermes_home / relative_path).read_bytes()).hexdigest()
except OSError:
return False
return actual == expected

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Bound registry and entrypoint reads before verification

When an attacker with the stated ability to write $HERMES_HOME/scaffolde/ replaces the registry or a managed entrypoint with a very large file, integrity checking buffers the entire file with read_bytes, and the registry path additionally calls read_text and json.loads before its managed hash is checked. A hash mismatch therefore does not prevent memory exhaustion during status, prompt construction, or invocation. Stream hashes with an explicit size ceiling and verify the registry bytes before parsing them.

Useful? React with 👍 / 👎.

Comment on lines +193 to +194
elif {"HOME", "PATH"} & set(env_set):
errors.append(_err("environment_override", "environment.set may not override HOME or PATH", cid or None))

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Compare protected environment names case-insensitively

On Windows, environment names are case-insensitive, so a descriptor using Path or path in environment.set bypasses this exact-case PATH check and replaces the child process's effective PATH. A capability script that subsequently launches a command by name can then execute an unintended binary despite the documented no-PATH-override rule. Normalize environment names for validation and reject case-insensitive duplicates before building the child environment.

AGENTS.md reference: AGENTS.md:L58-L64

Useful? React with 👍 / 👎.

Comment on lines +450 to +452
shell=False,
text=True,
capture_output=True,

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Decode Bun output explicitly as UTF-8

On hosts whose preferred locale encoding is not UTF-8, particularly Windows, text=True decodes Bun's UTF-8 output with the platform default encoding. Capability results containing non-ASCII email subjects or bodies can therefore become mojibake or raise a decode error that is reported as execution_failed even though the operation succeeded. Specify encoding="utf-8" with an explicit error policy when capturing the streams.

AGENTS.md reference: AGENTS.md:L58-L64

Useful? React with 👍 / 👎.

Comment on lines +402 to +403
reason = f"Scaffolde capability {capability_id}.{operation} is a write operation and requires approval. {detail}"
return reason, f"scaffolde:{capability_id}:{operation}:{digest}", preview_path

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Encode approval rule components unambiguously

Because capability IDs and operation names are only required to be non-empty strings, they may contain :. Two distinct writes such as capability a:b operation c and capability a operation b:c, with the same arguments, produce the same approval rule key here; a session or permanent approval for one therefore auto-approves the other. Hash a canonical structured tuple or restrict identifier characters rather than concatenating unescaped components.

Useful? React with 👍 / 👎.

@pai-scaffolde pai-scaffolde changed the title feat(scaffolde): native capability runtime with descriptor registry feat(scaffolde): register native projected capabilities Jul 27, 2026
@pai-scaffolde
pai-scaffolde merged commit 296bfb5 into main Jul 27, 2026
36 checks passed
@pai-scaffolde
pai-scaffolde deleted the feat/scaffolde-capability-runtime branch July 27, 2026 00:52
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant