Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 29 additions & 1 deletion docs/deployment/register-external-component.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -30,12 +30,14 @@ The component owner must operate its service, sockets, credentials, resources, a
Version 1 does not support these operations or configurations:

- Resume, reuse, repair, rebuild, recreation, or migration.
- Providerless Agent Policy Framework (APF) interceptor onboarding.
- Providerless component onboarding with NemoClaw-supplied initial policy.
- An externally supervised OpenShell gateway.
- More than one registered component.
- Generic shell commands, repository commands, vendor adapters, or a new OpenShell provider attachment API.

Onboarding without the declaration uses the existing onboarding behavior.
Registration alone does not select providerless onboarding.
Ordinary onboarding with a registered component keeps NemoClaw's initial policy and provider setup.

## Create the Declaration

Expand Down Expand Up @@ -129,6 +131,32 @@ The policy version must match the live sandbox observation.

NemoClaw sends identity to the component only after these checks pass.

<AgentOnly variant="openclaw">

### Select Interceptor-Supplied Policy

To use the registered component for fresh providerless onboarding, explicitly select `--apf-interceptor`:

```bash
nemoclaw onboard --fresh --apf-interceptor --name component-sandbox
```

The component must supply sandbox policy through the existing OpenShell `CreateSandbox` interceptor contract.
A generic component can implement this contract; no specific policy framework is required.
The [providerless onboarding prerequisites](../reference/commands#--apf-interceptor) also apply.

NemoClaw supplies no initial policy, creates no providers, and handles no component credentials in this flow.
Any providers required by the component's policy must resolve through existing OpenShell mechanisms before activation.
Missing, invalid, or unresolved policy or provider requirements stop onboarding before activation.
NemoClaw does not substitute its own policy or switch to ordinary onboarding.

After OpenShell creates the sandbox, NemoClaw verifies its immutable identity and effective policy before invoking the same activation endpoint described below.
Completion requires successful bounded activation and post-response verification.
This flow skips ordinary agent configuration, policy selection, and inference setup.
Providerless onboarding without a registered component retains its existing behavior.

</AgentOnly>

## Implement the Activation Endpoint

NemoClaw sends one HTTP/1.1 request over `activationSocketPath`.
Expand Down
10 changes: 9 additions & 1 deletion docs/reference/commands.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -449,7 +449,9 @@ Without the lock, it preserves the foreign reservation.

#### `--apf-interceptor`

Use this option to request a policyless sandbox creation for an APF-interceptor flow. This option currently supports providerless sandbox creation only. APF onboarding with an inference provider and model is not yet supported. APF onboarding with an OpenShell provider for a Model Context Protocol (MCP) server is also not yet supported. OpenShell cannot bind provider attachment to the new sandbox's verified immutable ID. If the prepared plan contains any provider, NemoClaw exits before it:
Use this option to request sandbox creation without a NemoClaw-supplied policy.
This option supports providerless onboarding only; NemoClaw does not configure an inference provider, model, or credential provider in this mode.
If NemoClaw's prepared plan contains any provider, it exits before it:

- Creates the sandbox.
- Registers or changes credentials.
Expand All @@ -465,6 +467,12 @@ The option requires these conditions:

NemoClaw omits a caller policy from every sandbox creation attempt. After creation, NemoClaw binds the returned durable sandbox identity and verifies its effective policy. The policy must be sandbox-scoped and contain every policy entry required by the prepared configuration. NemoClaw stores no policy owner, receipt, or desired state; later policy commands read OpenShell directly. This verification does not establish that APF injected the policy.

With one [registered external component](../deployment/register-external-component#select-interceptor-supplied-policy), the component supplies policy through the existing OpenShell interceptor contract.
Required providers must resolve through existing OpenShell mechanisms before activation.
NemoClaw activates the component only after verifying immutable sandbox identity and effective policy, then verifies them again after the response.
Missing or invalid policy does not trigger a fallback to NemoClaw policy or ordinary onboarding.
Registration alone does not select this option.

```bash
$$nemoclaw onboard --fresh --apf-interceptor --name my-apf-sandbox
```
Expand Down
24 changes: 10 additions & 14 deletions src/lib/onboard/external-component/onboarding.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -218,18 +218,14 @@ describe("external component onboarding lifecycle", () => {
}
});

it("rejects APF onboarding when a component is registered (#11340)", () => {
mocks.loadExternalComponentDeclaration.mockReturnValue({} as never);

expect(() =>
prepareExternalComponent({
externalComponentActivation: null,
apfInterceptorRequested: true,
}),
).toThrowError(
expect.objectContaining<Partial<ExternalComponentContractError>>({
code: "lifecycle_unsupported",
}),
);
});
it.each([true, false])(
"preserves registration with providerless selection %s (#11486)",
(apfInterceptorRequested) => {
const component = {} as PreparedExternalComponent;
mocks.loadExternalComponentDeclaration.mockReturnValue(component);
expect(prepareExternalComponent({ apfInterceptorRequested })).toBe(component);
mocks.loadExternalComponentDeclaration.mockReturnValue(null);
expect(prepareExternalComponent({ apfInterceptorRequested })).toBeNull();
},
);
});
6 changes: 1 addition & 5 deletions src/lib/onboard/external-component/onboarding.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,11 +20,7 @@ export function prepareExternalComponent(
} | null,
): PreparedExternalComponent | null {
assertNoIncompleteExternalComponentActivation(session);
const externalComponent = loadExternalComponentDeclaration();
if (externalComponent && session?.apfInterceptorRequested === true) {
throw new ExternalComponentContractError("lifecycle_unsupported");
}
return externalComponent;
return loadExternalComponentDeclaration();
}

export function assertNoIncompleteExternalComponentActivation(
Expand Down
55 changes: 40 additions & 15 deletions src/lib/onboard/external-component/proof.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,24 +52,49 @@ function fixture() {
}

describe("external component activation proof", () => {
it("binds the durable OpenShell identity to the effective policy (#11340)", () => {
const { deps } = fixture();
it.each([policyHash, policyHash.slice("sha256:".length)])(
"binds the durable OpenShell identity to policy digest %s (#11486)",
(hash) => {
const { deps, inspection } = fixture();
inspection.policyIdentity.hash = hash;

const proof = createExternalComponentActivationProof("assistant", "nemoclaw", deps);

expect(proof).toMatchObject({
gatewayName: "nemoclaw",
sandboxId,
sandboxIdentityFingerprint: `sha256:${fingerprintOpenShellSandboxId(sandboxId)}`,
lifecycleGeneration: "generation-1",
policySource: "sandbox",
policyHash,
policyActiveVersion: 3,
});
expect(deps.inspectPolicy).toHaveBeenCalledWith(
"assistant",
"verify external component activation policy",
"nemoclaw",
);
},
);

it("rejects a changed OpenShell digest after activation (#11486)", () => {
const { deps, inspection } = fixture();
inspection.policyIdentity.hash = "a".repeat(64);
const proof = createExternalComponentActivationProof("assistant", "nemoclaw", deps);
inspection.policyIdentity.hash = "b".repeat(64);
expect(() => proof.revalidate("after_activation")).toThrow(ExternalComponentProofError);
});

expect(proof).toMatchObject({
gatewayName: "nemoclaw",
sandboxId,
sandboxIdentityFingerprint: `sha256:${fingerprintOpenShellSandboxId(sandboxId)}`,
lifecycleGeneration: "generation-1",
policySource: "sandbox",
policyHash,
policyActiveVersion: 3,
});
expect(deps.inspectPolicy).toHaveBeenCalledWith(
"assistant",
"verify external component activation policy",
"nemoclaw",
it.each([
"a".repeat(63),
"a".repeat(65),
`sha256:sha256:${"a".repeat(64)}`,
`sha512:${"a".repeat(64)}`,
])("rejects malformed policy digest %s (#11486)", (hash) => {
const { deps, inspection } = fixture();
inspection.policyIdentity.hash = hash;
expect(() => createExternalComponentActivationProof("assistant", "nemoclaw", deps)).toThrow(
ExternalComponentProofError,
);
});

Expand Down
5 changes: 3 additions & 2 deletions src/lib/onboard/external-component/proof.ts
Original file line number Diff line number Diff line change
Expand Up @@ -81,11 +81,12 @@ function captureProofSnapshotUnchecked(
"verify external component activation policy",
expectedGatewayName,
);
const policyDigest = policy.inspection.policyIdentity.hash.replace(/^sha256:/u, "");
if (
policy.gatewayName !== expectedGatewayName ||
policy.inspection.policySource !== "sandbox" ||
row.current_policy_version !== policy.inspection.policyIdentity.activeVersion ||
!/^sha256:[0-9a-f]{64}$/u.test(policy.inspection.policyIdentity.hash)
!/^[0-9a-f]{64}$/u.test(policyDigest)
) {
throw new ExternalComponentProofError();
}
Expand All @@ -101,7 +102,7 @@ function captureProofSnapshotUnchecked(
gatewayName: expectedGatewayName,
lifecycleGeneration: entry.lifecycleGeneration,
policyActiveVersion: policy.inspection.policyIdentity.activeVersion,
policyHash: policy.inspection.policyIdentity.hash,
policyHash: `sha256:${policyDigest}`,
policySource: policy.inspection.policySource,
sandboxId: row.id,
sandboxIdentityFingerprint: `sha256:${fingerprint}`,
Expand Down
Loading
Loading