Skip to content

tools: drop ownerSkillVersionHash denormalization#32132

Merged
dvargasfuertes merged 1 commit into
mainfrom
apollo/drop-owner-skill-version-hash
May 26, 2026
Merged

tools: drop ownerSkillVersionHash denormalization#32132
dvargasfuertes merged 1 commit into
mainfrom
apollo/drop-owner-skill-version-hash

Conversation

@vellum-apollo-bot
Copy link
Copy Markdown
Contributor

What

Continuing the Tool field-by-field triage. Drops ownerSkillVersionHash from Tool — including the assistant-side type, contracts-side type, the IPC schema + relay, the IPC client payload builder, skill-tool-factory, meet-manifest-loader, and the registry's plugin-clear.

Why

The field had zero production consumers. The tamper-detection security mechanism that matters — blocking execution when a skill's source changed since approval — is the expectedSkillVersionHash option captured into each skill tool's execute() closure, checked inside runSkillToolScript / runSkillToolScriptSandbox. Nothing reads the field off the Tool object at runtime:

  • executor.ts does not reference it.
  • The permission system does not reference it.
  • The daemon-side IPC buildProxyTool carries the hash onto the daemon-side Tool, but the supervisor short-circuit replaces that proxy's execute closure with the manifest's, and nothing reads the field on the daemon side either.

So the field is pure write-only denormalization on top of an already-correct closure-based mechanism.

Side cleanup

In meet-manifest-loader.ts, the field's removal left buildProxyTool's manifestHash param dead. Dropped the param and updated the single callsite.

Test changes

  • Deleted the dedicated 110-line "version hash plumbing to projected tools" suite in conversation-skill-tools.test.ts. The suite's comment block claimed executor.ts uses the field to build policy context — stale; executor.ts has no reference to the field. Re-aiming the assertions would only verify that the test's own mock factory received the right arg, which tests the mock, not the production code. Real tamper-detection coverage already lives in skill-script-runner-host.test.ts (5+ cases) and skill-script-runner.test.ts asserting expectedSkillVersionHash directly.
  • Deleted two skill-tool-factory.test.ts tests that asserted only the field ("sets ownerSkillVersionHash from versionHash", "passes versionHash through to all created tools").
  • Kept "execute() works correctly when versionHash is provided" since it exercises the real closure → runner integrity check; trimmed the stale field assertion at the tail.
  • Updated mock factories in 5 sibling tests and the spoofed-ownership case in plugin-tool-contribution.test.ts.
  • skills/meet-join/__tests__/entrypoint.test.ts's fakeTool helper also still carried ownerSkillBundled: true — a leftover from PR tools: drop ownerSkillBundled denormalization #32083 that the prior assistant packages grep scope missed. Cleaned both fields here.

Verification

  • bun run typecheck clean (assistant + contracts package).
  • bun run lint clean.
  • All 10 touched test files pass individually, plus skill-script-runner.test.ts and skill-script-runner-host.test.ts as a smoke check for the real tamper path.
  • A pre-existing cross-file mock.module pollution causes batch failures (5 specific test files combined → API_KEY_PROVIDERS SyntaxError, plus a drizzle-orm hoist artifact). Confirmed identical batch result on clean main via git stash baseline — not caused by this PR.

Field landscape after this PR

ToolDefinition: description?, defaultRiskLevel?, input_schema?, executionTarget?, execute?
LoadedTool = Required<ToolDefinition> & { name }
Tool extends LoadedTool: category, executionMode?, origin?,
  ownerSkillId?, ownerMcpServerId?, ownerPluginId?

Three fields down (executionTarget, ownerSkillBundled, ownerSkillVersionHash), with the broader triage now turning to the remaining owner fields and origin / executionMode / category.

Copy link
Copy Markdown
Contributor

@devin-ai-integration devin-ai-integration Bot left a comment

Choose a reason for hiding this comment

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

✅ Devin Review: No Issues Found

Devin Review analyzed this PR and found no potential bugs to report.

View in Devin Review to see 3 additional findings.

Open in Devin Review

ownerSkillVersionHash was a denormalization on Tool that had ZERO
production consumers. The tamper-detection security mechanism that
matters — blocking execution when a skill's source changed since
approval — is the `expectedSkillVersionHash` option captured into
each skill tool's `execute()` closure, checked inside
runSkillToolScript / runSkillToolScriptSandbox. Nothing reads the
field off the Tool object at runtime.

Same shape as PR #32083: removed from Tool (assistant + contracts),
IPC schema + buildProxyTool relay, the IPC client payload builder,
skill-tool-factory, meet-manifest-loader, and the registry's
plugin-clear.

In meet-manifest-loader the field's removal left buildProxyTool's
`manifestHash` param dead — dropped the param and updated the
single callsite.

Test changes:
- Deleted the dedicated 'version hash plumbing to projected tools'
  suite in conversation-skill-tools.test.ts (110 lines). The suite's
  comment claimed downstream executor.ts uses the field to build
  policy context — stale; executor.ts has no reference to the field.
  Re-aiming the assertions would only test that the test's own mock
  factory received the right arg, which tests the mock, not the
  production code. Real tamper-detection coverage already lives in
  skill-script-runner-host.test.ts (5+ cases) and
  skill-script-runner.test.ts asserting `expectedSkillVersionHash`
  directly.
- Deleted two skill-tool-factory tests that asserted only the field
  ('sets ownerSkillVersionHash from versionHash', 'passes versionHash
  through to all created tools'). Kept 'execute() works correctly
  when versionHash is provided' since it exercises the real closure
  → runner integrity check; trimmed the stale field assertion at
  the tail.
- Updated mock factories in 5 sibling tests and the spoofed-ownership
  case in plugin-tool-contribution.test.ts.
- skills/meet-join/__tests__/entrypoint.test.ts's fakeTool helper
  also still carried `ownerSkillBundled: true` — a leftover from
  PR #32083 that the prior `assistant packages` grep scope missed.
  Cleaned both fields here.

Typecheck and lint clean. All 10 touched test files pass individually
(plus skill-script-runner.test.ts and skill-script-runner-host.test.ts
as smoke check for the real tamper path). A pre-existing cross-file
mock.module pollution causes batch failures on clean main — confirmed
identical 0/9 batch result with `git stash` baseline.
@vellum-apollo-bot vellum-apollo-bot Bot force-pushed the apollo/drop-owner-skill-version-hash branch from b70b7c6 to 6bedf67 Compare May 26, 2026 18:39
@dvargasfuertes dvargasfuertes merged commit 5dca785 into main May 26, 2026
20 checks passed
@dvargasfuertes dvargasfuertes deleted the apollo/drop-owner-skill-version-hash branch May 26, 2026 18:56
vellum-apollo-bot Bot added a commit that referenced this pull request May 27, 2026
…ic OwnerInfo

Collapses three parallel ownership fields on Tool into a single
`owner?: OwnerInfo` where OwnerInfo is `{ kind: 'skill' | 'plugin' | 'mcp', id: string }`.

Why:
- Three fields encoded the same shape (kind + id) and the consumer code
  branched on origin to pick which to read. One field with discriminated
  kind makes that branching unnecessary.
- New owner kinds can be added without growing the Tool type.
- Same shape used end-to-end (registry, IPC manifest, contracts).

Behavior preserved:
- Plugin spoof-defense in registerPluginTools still overwrites owner via
  spread + explicit assignment. Test updated to spoof the new shape.
- Skill/plugin/MCP conflict-detection paths now read `existing.owner?.kind`
  and `existing.owner?.id` instead of `existing.ownerSkillId` etc.
- IPC ToolManifestSchema in skill-routes/registries.ts now takes
  `owner: { kind: 'skill', id: string }` (strict to kind: 'skill' since
  only skill-owned tools flow over IPC; plugin and MCP tools live in-process).

Log fields kept as-is:
- Tool-registered log lines in registry.ts still emit `ownerSkillId`,
  `ownerPluginId`, `ownerMcpServerId` keys so downstream log consumers
  that grep on those names continue to work. The structured log key is
  intentionally decoupled from the Tool-type field shape.

Test surface migrated: 11 unit + IPC test files (47 references).
All targeted tests green, typecheck clean, lint clean.

Field 5 in the Tool field-by-field triage (following PR #32083, #32132).
vellum-apollo-bot Bot added a commit that referenced this pull request May 27, 2026
…ic OwnerInfo

Collapses three parallel ownership fields on Tool into a single
`owner?: OwnerInfo` where OwnerInfo is `{ kind: 'skill' | 'plugin' | 'mcp', id: string }`.

Why:
- Three fields encoded the same shape (kind + id) and the consumer code
  branched on origin to pick which to read. One field with discriminated
  kind makes that branching unnecessary.
- New owner kinds can be added without growing the Tool type.
- Same shape used end-to-end (registry, IPC manifest, contracts).

Behavior preserved:
- Plugin spoof-defense in registerPluginTools still overwrites owner via
  spread + explicit assignment. Test updated to spoof the new shape.
- Skill/plugin/MCP conflict-detection paths now read `existing.owner?.kind`
  and `existing.owner?.id` instead of `existing.ownerSkillId` etc.
- IPC ToolManifestSchema in skill-routes/registries.ts now takes
  `owner: { kind: 'skill', id: string }` (strict to kind: 'skill' since
  only skill-owned tools flow over IPC; plugin and MCP tools live in-process).

Log fields kept as-is:
- Tool-registered log lines in registry.ts still emit `ownerSkillId`,
  `ownerPluginId`, `ownerMcpServerId` keys so downstream log consumers
  that grep on those names continue to work. The structured log key is
  intentionally decoupled from the Tool-type field shape.

Test surface migrated: 11 unit + IPC test files (47 references).
All targeted tests green, typecheck clean, lint clean.

Field 5 in the Tool field-by-field triage (following PR #32083, #32132).
dvargasfuertes pushed a commit that referenced this pull request May 27, 2026
)

* tools: replace ownerSkillId/ownerPluginId/ownerMcpServerId with generic OwnerInfo

Collapses three parallel ownership fields on Tool into a single
`owner?: OwnerInfo` where OwnerInfo is `{ kind: 'skill' | 'plugin' | 'mcp', id: string }`.

Why:
- Three fields encoded the same shape (kind + id) and the consumer code
  branched on origin to pick which to read. One field with discriminated
  kind makes that branching unnecessary.
- New owner kinds can be added without growing the Tool type.
- Same shape used end-to-end (registry, IPC manifest, contracts).

Behavior preserved:
- Plugin spoof-defense in registerPluginTools still overwrites owner via
  spread + explicit assignment. Test updated to spoof the new shape.
- Skill/plugin/MCP conflict-detection paths now read `existing.owner?.kind`
  and `existing.owner?.id` instead of `existing.ownerSkillId` etc.
- IPC ToolManifestSchema in skill-routes/registries.ts now takes
  `owner: { kind: 'skill', id: string }` (strict to kind: 'skill' since
  only skill-owned tools flow over IPC; plugin and MCP tools live in-process).

Log fields kept as-is:
- Tool-registered log lines in registry.ts still emit `ownerSkillId`,
  `ownerPluginId`, `ownerMcpServerId` keys so downstream log consumers
  that grep on those names continue to work. The structured log key is
  intentionally decoupled from the Tool-type field shape.

Test surface migrated: 11 unit + IPC test files (47 references).
All targeted tests green, typecheck clean, lint clean.

Field 5 in the Tool field-by-field triage (following PR #32083, #32132).

* tools: move ownership from Tool to registry-side ownersByName map

Addresses the round-1 review on PR #32224. The Tool object no longer
carries an owner field — ownership lives exclusively on the registry,
keyed by tool name, and is read through a new getToolOwner(name)
accessor. Callers that need to know who owns a tool now go through the
registry instead of trusting a manifest-supplied field.

## Architecture

- New ownersByName: Map<string, OwnerInfo> in registry.ts populated
  exclusively by the register*Tools functions. The only way to claim a
  tool is to go through a register* function, so callers cannot spoof
  ownership by forging a field on the Tool literal.
- New exported getToolOwner(name): OwnerInfo | undefined as the single
  read accessor. Core tools have no entry — returns undefined.
- registerTool (bare install) intentionally does NOT record ownership.
  Only registerSkillTools / registerPluginTools / registerMcpTools /
  registerExternalTools / registerAppTools / registerSystemTools /
  registerUiSurfaceTools populate ownersByName, each stamping owner
  from their argument list.

## Production-side changes

- tools/types.ts + packages/skill-host-contracts/src/tool-types.ts:
  dropped owner?: OwnerInfo from the Tool type; OwnerInfo docstring
  points at getToolOwner.
- registry.ts: register*/unregister*/externalToolProviders all updated.
  registerExternalTools(owner, toolsOrProvider) — owner is now a
  required first argument. externalToolProviders entries are
  {owner, provider} tuples; initializeTools propagates owner into
  ownersByName at provider-resolution time.
- IPC: register_tools params now carry skillId at the top level
  alongside tools (one frame = one skill's batch). The per-tool
  manifest schema no longer carries an owner field. The daemon-side
  handler calls registerSkillTools(skillId, proxies). Connection
  ref-count mirroring simplifies to a single
  conn.addSkillToolsOwner(skillId).
- skill-host-contracts client: sends {skillId, tools: manifests}.
- daemon-skill-host adapter wraps the single-arg facet method,
  deriving owner from the skillId closure.
- skill-tool-factory: dropped the skillId parameter from
  createSkillTool / createSkillToolsFromManifest — ownership is now
  recorded by registerSkillTools, not stamped on the Tool object.
  Updated all call sites (conversation-skill-tools, meet-manifest-loader).
- Non-registry consumers now read ownership via getToolOwner(name):
  permissions/checker (isToolOwnerSkillBundled),
  tools/tool-approval-handler (load-hint logic),
  daemon/conversation-skill-tools (post-registration filter).
- providers-setup, mcp-reload-service: registerMcpTools(serverId, tools).

## Logging

Preserved the ownerSkillId log field key on the existing log calls so
log consumers don't break. The field still resolves to the same string
the registry now stores in ownersByName.

## Tests

- registry.test.ts: imported getToolOwner; simplified makeSkillTool
  signature (no owner stamp); rewrote 'origin metadata' / 'dynamic
  skill tool registry' / 'skill tool reference counting' blocks.
- Test mocks for skill-tool-factory and registry updated to match the
  new signatures across conversation-skill-tools.test.ts,
  conversation-app-control-instantiation.test.ts,
  skill-projection-feature-flag.test.ts. Each mock derives ownership
  from its own mockRegisteredTools keying (since the real registry
  derives it from the register*Tools argument).
- plugin-tool-contribution.test.ts: ownership assertions go through
  getToolOwner(name) instead of reading retrieved?.owner.
- registries.test.ts (IPC): handler params now include skillId at
  the top level; added a 'rejects missing skillId' test that
  asserts the new zod schema enforces it.
- meet-manifest-loader.test.ts: mock signature mirrors
  registerExternalTools(owner, provider); one test now asserts the
  captured owner is {kind: 'skill', id: 'meet-join'}.
- checker.test.ts: mock skill tools registered via
  registerSkillTools(skillId, [tool]) so isToolOwnerSkillBundled
  resolves through the registry.

## Verification

- bunx tsc --noEmit: clean across assistant + skill-host-contracts.
- All touched test files pass when run individually
  (Bun's mock.module is global so cross-file runs hit pre-existing
  contamination, identical 29 fails on main vs this branch).
- Linted all touched files; clean.

* tools: wire getToolOwner through tool-executor.test.ts registry mock

CI surfaced a single failure on PR #32224 — `ToolExecutor allowedToolNames
gating > inactive skill tool names the owning skill in the load hint`. Root
cause: the file-local `mock.module("../tools/registry.js", ...)` only stubbed
`getTool` and `getAllTools`, so production's new `getToolOwner(name)` import
resolved to `undefined`, dropping the load-hint owner branch and producing
the generic "Load the skill that provides this tool first." message.

Wire the mock's `getToolOwner` to surface the optional `owner` field from
the override-produced tool. The three tests that already encode
`owner: { kind: "skill", id: ... }` inline keep working unchanged; the
failing test now resolves the owner the same way production does.

Verified: bun test src/__tests__/tool-executor.test.ts → 80/80 pass.
bunx tsc --noEmit clean. eslint clean.

---------

Co-authored-by: vellum-apollo-bot[bot] <242025090+vellum-apollo-bot[bot]@users.noreply.github.com>
Co-authored-by: vellum-apollo-bot[bot] <209693332+vellum-apollo-bot[bot]@users.noreply.github.com>
Co-authored-by: vellum-apollo-bot[bot] <234526108+vellum-apollo-bot[bot]@users.noreply.github.com>
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