Skip to content

feat: let local agents delegate tasks to Moca - #455

Closed
moonlight16 wants to merge 5 commits into
rossoctl:mainfrom
moonlight16:feat/moca-context-delegation
Closed

moonlight16 wants to merge 5 commits into
rossoctl:mainfrom
moonlight16:feat/moca-context-delegation

Conversation

@moonlight16

@moonlight16 moonlight16 commented Oct 6, 2026 •

Copy link
Copy Markdown
Member

What this enables

A local agent can delegate one task or fan out many tasks to Moca. The agent selects the relevant local context files, and Context Service transports them into a shared PVC in Moca. Moca then runs the delegated tasks in isolated Sandboxes with that context mounted read-only and returns the results to the local agent.

This supports workflows such as continuing work after closing a laptop, using cloud resources unavailable locally, and parallelizing many bounded subtasks.

How it works

  • add the moca-delegate-task skill for local agent harnesses
  • create an isolated Moca workload with one or more Sandboxes
  • export the selected local context into one portable bundle
  • authorize a short-lived upload through Moca
  • use Context Service to materialize the bundle in the workload PVC
  • mount the activated context revision read-only in every delegated Sandbox
  • dispatch one task or fan out a batch across the Sandbox pool
  • return task results and report which context files were used
  • wait for Sandbox pods to release storage before cleaning up the workload

Context Service counterpart: rossoctl/context-service#50
Related to #378.

Validation

  • 34 focused TypeScript tests
  • TypeScript typecheck
  • 27 Python helper and demo tests
  • ShellCheck for the Kind setup and demo scripts
  • local Kind demo: 2/2 delegated incident tasks matched across two Sandboxes
  • verified transient workload resources and local demo contexts were removed

Comment thread packages/knative-server/test/workload-runtime.test.ts Fixed
@moonlight16 moonlight16 changed the title feat: delegate context-aware tasks to Moca feat: let local agents delegate tasks to Moca Oct 7, 2026
Signed-off-by: Jeremy Cohn <Jeremy.Cohn@ibm.com>
Signed-off-by: Jeremy Cohn <Jeremy.Cohn@ibm.com>
Signed-off-by: Jeremy Cohn <Jeremy.Cohn@ibm.com>
Signed-off-by: Jeremy Cohn <Jeremy.Cohn@ibm.com>
Signed-off-by: Jeremy Cohn <Jeremy.Cohn@ibm.com>
@moonlight16
moonlight16 force-pushed the feat/moca-context-delegation branch from 9157028 to 27a2f96 Compare October 7, 2026 06:10

@pdettori pdettori left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Summary

This adds context delegation from local agents. The context path itself is careful: traversal checks, SHA-256 revision validation, a read-only mount at both the PVC and the mount level, and hidden claim names. Thanks for fixing DCO, lint and CodeQL in the latest push; CI is green.

P6: the PR is built entirely on deploy/knative, which RA1 is deprecating. On deploy/k8s, only prompt-only /runs works; workloads, upload and activate don't (see the comment on workload-runtime.ts). Per #378 item 5 this needs a P6-native path, or at least a Knative-only flag plus a follow-up issue. It also overlaps with #381, which plans mocactl subagent commands, so the client may fit better there than as a separate Python script.

Native workload path: it is on by default, with no size, StorageClass or quota limits, broad PVC-delete RBAC, and re-creating a live name leaks resources. Please add tests for re-create, quota/size validation, and the activate-vs-delete race along with the fixes.

Areas reviewed: TypeScript, K8s manifests/RBAC, Shell, Python, Docs, P6-on-kube compatibility
Commits: 5, all signed-off
CI status: passing

Assisted-By: Claude Code

};
}

export async function createWorkloadRuntime(

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

must-fix — doesn't work on P6-on-kube. This path runs kubectl apply for its own sleep infinity Sandbox resources (the Knative pod-exec model). On deploy/k8s the supervisor runs this same server.ts handler, but its ServiceAccount has no RBAC and automountServiceAccountToken: false (deploy/k8s/base/supervisor.yaml), so every POST /workloads returns 502. On main that route returned 501 without Context Service, so this is a regression on P6. Even with cluster access, these pods never register with sandbox-relay, and records discovery (harness/src/select-sandbox.ts) ignores sandboxPoolSelector, so runs land on a shared remote-worker with no /workspace mount.

#378 item 5 says the Knative/KEDA path isn't extended (RA1) and new work must run on deploy/k8s. Please either deliver context through the P6 sandbox model (a sandbox tier with a context volume in deploy/k8s/base/sandbox.yaml, selected via SH_SANDBOX_TIERS from #425, or upload through relay SandboxExec), or put this behind an off-by-default flag, label it Knative-only, and file a follow-up for P6.

res.writeHead(409, JSON_HEADERS).end(JSON.stringify({ error: 'workload_name_taken' }));
return;
}
if (!usesContext) {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

must-fix. Before this PR, POST /workloads returned 501 unless CONTEXT_SERVICE_URL was set. Native provisioning is now on in every deployment, and the harness ServiceAccount gains Sandbox create/delete and PVC delete. Please gate native provisioning and the new /uploads and /activate routes behind a feature flag that is off by default.

@@ -547,14 +598,88 @@ async function handleCreateWorkload(req: IncomingMessage, res: ServerResponse):
res.writeHead(409, JSON_HEADERS).end(JSON.stringify({ error: 'workload_name_taken' }));

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

must-fix. The 409 check only refuses a live workload owned by another subject. If the same subject re-POSTs a live name, it re-applies over the existing Sandboxes, and saveWorkload overwrites the record. That drops contextName/contextId, so the Context leaks, and a smaller sandboxes count orphans the extras. With auth off, every anonymous caller counts as the same owner, so this works across clients too. Refuse any existing record whose status isn't deleted, or make re-create a true no-op.

replicas,
workspace: {
kind: 'native',
size: spec.workspace?.size ?? '1Gi',

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

must-fix. workspace.size and storageClass go into volumeClaimTemplates without validation. A caller can request up to 100 Sandboxes per workload, with no limit on workloads, each with a PVC of any size on any StorageClass. Please add a size ceiling, a StorageClass allowlist, and a per-subject quota.

verbs: ['get', 'list']
verbs: ['create', 'get', 'list', 'patch', 'update', 'delete']
- apiGroups: ['']
resources: ['persistentvolumeclaims']

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

suggestion. persistentvolumeclaims: delete covers every PVC in the namespace, including the leaf-work and Redis volumes. Narrow it (dedicated namespace or label guard), or at least document the blast radius.

}
const publicBase = new URL(`${publicBaseUrl()}/`);
if (
!['http:', 'https:'].includes(publicBase.protocol) ||

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

suggestion. CONTEXT_SERVICE_PUBLIC_URL may be plain http:, which sends the upload bearer token in cleartext. Require https: except for loopback.

})
try:
workload = create_workload(
url, workload_id, args.sandboxes, False, args.timeout, args.poll_interval,

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

suggestion. run always sends shared=False, so --sandboxes > 1 always gets 400 shared_workspace_required. Pass shared=args.sandboxes > 1.



def export_context(context: str, bundle: Path) -> dict[str, Any]:
result = contextctl("ctx", "export", context, "--output", str(bundle))

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

suggestion. The whole export is uploaded with no client-side check. A cheap denylist on the manifest before the PUT (.env, *.pem, id_rsa*, .git/config) would catch accidental secret uploads.

kubectl --context "$KUBE_CONTEXT" apply -f -
kubectl --context "$KUBE_CONTEXT" rollout restart deployment/context-service -n default
else
curl -fsSL "https://raw.githubusercontent.com/rossoctl/context-service/${CONTEXT_SERVICE_REF}/deploy/context-service.yaml" |

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

suggestion. context-service.yaml is applied from the mutable main ref. Pin CONTEXT_SERVICE_REF to a tag or SHA.

account_args=(--account "$MOCA_OP_ACCOUNT")
fi
local credential
credential="$(op read "${account_args[@]}" "$MOCA_ANTHROPIC_OP_REF")" || die "could not read the Anthropic credential from 1Password"

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

nit. Under set -u, "${account_args[@]}" on an empty array fails on macOS's bash 3.2 when MOCA_OP_ACCOUNT is unset. Use ${account_args[@]+"${account_args[@]}"}.

pdettori pushed a commit that referenced this pull request Oct 9, 2026
Moca's /workloads asked Context Service to create sandbox pools (/v1/sandbox-pools). Moca will own its workloads and sandboxes, and Context Service will only store context (rossoctl/context-service#46).

Remove Moca's use of the sandbox-pool API. Until Moca provisions workloads itself, the /workloads routes, and any /runs request that names a workloadId, answer 501 workloads_unavailable. Runs without a workload are unchanged.

Split out of #455.

Assisted-By: Claude (Anthropic AI) <noreply@anthropic.com>
Signed-off-by: Jeremy Cohn <Jeremy.Cohn@ibm.com>
pdettori added a commit to pdettori/moca that referenced this pull request Oct 9, 2026
…#473)

Resolve server.ts in main's favour for the workload lifecycle — every
/workloads route and any run naming a workloadId now answers 501
workloads_unavailable (rossoctl#455) — keeping this branch's error-code changes on
top. The spec follows: the /v1/runs 404 loses workload_not_found, the 501
gains workloads_unavailable, and the sandboxPoolSelector note drops the
workload-resolver clause. routes.ts workload summaries note the 501.

Assisted-By: Claude (Anthropic AI) <noreply@anthropic.com>
Signed-off-by: Paolo Dettori <dettori@us.ibm.com>
moonlight16 added a commit to rossoctl/context-service that referenced this pull request Oct 9, 2026
Context Service created Moca's execution environments through /v1/sandbox-pools: Sandboxes, sandbox profiles, and WarmPool claims. Moca now owns its sandboxes, and rossoctl/moca#469 removed its last call to this API.

Remove the sandbox-pool API, the Sandbox, profile and WarmPool code, contextctl sandbox-pool, CS_SANDBOX_IMAGE, and the matching RBAC. Context storage is unchanged.

Closes #46. Part of the work split out of rossoctl/moca#455.

Assisted-By: Claude (Anthropic AI) <noreply@anthropic.com>

Signed-off-by: Jeremy Cohn <Jeremy.Cohn@ibm.com>
moonlight16 added a commit to rossoctl/context-service that referenced this pull request Oct 9, 2026
Moca creates a Context, gives the client a one-time upload capability, freezes the uploaded revision, and downloads that frozen revision as a portable bundle. Context Service stores the bytes and never creates sandboxes.

Ported from #50 without the PVC attachment endpoint, which bundle delivery does not need. Adds the frozen-bundle export endpoint, aborts a failed export instead of returning a truncated 200, and starts without a helper image (upload and export are then unavailable).

Part of the work split out of rossoctl/moca#455.

Assisted-By: Claude (Anthropic AI) <noreply@anthropic.com>

Signed-off-by: Jeremy Cohn <Jeremy.Cohn@ibm.com>
pdettori pushed a commit that referenced this pull request Oct 9, 2026
Workloads created through the old /workloads routes are no longer released by Moca or Context Service. Document how to delete their Sandboxes, PVCs, and sh:workload:* Redis keys, and point the docs at #476 instead of #455.

Follow-up to #469.

Assisted-By: Claude (Anthropic AI) <noreply@anthropic.com>
Signed-off-by: Jeremy Cohn <Jeremy.Cohn@ibm.com>
moonlight16 added a commit to moonlight16/moca that referenced this pull request Oct 11, 2026
A workload belongs to the user who created it and owns one Context in Context Service. The user creates the workload, uploads a context bundle directly to Context Service with a one-time grant, and activates it to freeze that revision. Deleting the workload deletes the Context.

Off by default (MOCA_CONTEXT_WORKLOADS_ENABLED=1 and CONTEXT_SERVICE_URL). Every route requires the user's control-plane API token. Names are scoped per user (first half of rossoctl#358). Per-user quota, a storage size ceiling, a StorageClass allowlist, and atomic lifecycle transitions. Runs that name a workload still answer 501 until context delivery lands.

Part of the work split out of rossoctl#455.

Assisted-By: Claude (Anthropic AI) <noreply@anthropic.com>
Signed-off-by: Jeremy Cohn <Jeremy.Cohn@ibm.com>
pdettori pushed a commit that referenced this pull request Oct 12, 2026
A synchronous /runs request can name a workloadId. Moca checks that the session token's user owns the ready workload, downloads its frozen revision from Context Service once, and copies it into the shared sandbox through SandboxExec as raw bytes. The sandbox verifies the sha256 and publishes it read-only; it is removed after the run by default, and each user's copy is cached separately.

Shared, running sandboxes cannot mount the workload's PVC, so phase 1 copies; phase 2 (#476) mounts it instead.

Part of the work split out of #455.

Assisted-By: Claude (Anthropic AI) <noreply@anthropic.com>
Signed-off-by: Jeremy Cohn <Jeremy.Cohn@ibm.com>
pdettori pushed a commit that referenced this pull request Oct 14, 2026
A living list of the ways we expect people to move context into and out of Moca: what sandbox and storage each workflow uses, and where it stands. Centralizes the workflows table that lived in the delegate skill in #455.

Assisted-By: Claude (Anthropic AI) <noreply@anthropic.com>
Signed-off-by: Jeremy Cohn <Jeremy.Cohn@ibm.com>
@moonlight16

Copy link
Copy Markdown
Member Author

This PR has been split into smaller PRs, which address the review above. Keeping it open as a draft for reference until the delegate skill and its diagram land in their own PR; then I will close it.

Merged:

In review:

Design:

@moonlight16

Copy link
Copy Markdown
Member Author

All of this PR has now landed in smaller PRs, the last being #535 (moca-delegate-task rebuilt on mocactl, with the delegation diagram) and #544 (Kind demo for the skill). The full list is in the comment above. Closing; phase 2 (per-workload sandboxes and storage) continues in #476.

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.

3 participants