Skip to content
Open
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
8 changes: 4 additions & 4 deletions .kilo/skills/kilo-mcp/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -48,14 +48,14 @@ They do not expose API `search` or `call`.
1. Launch only when the user explicitly requests a Cloud Agent session. Children have independent sandboxes/worktrees; use local tools for this worktree.
2. Resolve the requested model or established user preference with `search_models`; copy its exact `id`. Otherwise browse with `{}` and ask the user to choose. No implicit default: `isPreferred` is a recommendation. Omit `variant` unless requested; pass only a supported variant separately. Never guess or substitute IDs. `alias_target` names a floating alias's target; discovery does not prove policy approval.
3. Call `spawn_session` with repository, self-contained task prompt, mode, exact model ID, and fresh UUID `operationKey`. Include constraints, verification, and Git/PR instructions. It starts immediately with multi-chat support. Organization and `createdOnPlatform` are inherited, not caller inputs.
4. Set `callbackTarget: { "url": "session:parent" }` for completion/failure/interruption messages; omit for none. Prefer callbacks over polling; never wait in a tool for one.
4. Set `callbackTarget: { "url": "session:parent" }` to notify the canonical parent chat, or `{ "url": "workspace:new-chat" }` to open a new chat in the parent workspace with the result as its first message. Omit for none. Prefer callbacks over polling; never wait in a tool for one.
5. `list_child_sessions` recovers IDs and `initialMessageId` when recorded; `get_session` checks status; `get_message_result` reads outcomes. Before a new authorized follow-up, call `generate_message_id({})` and copy its `messageId` into `send_message`. This generates no work. Retain it for retries; never replace it after an uncertain send. Initial tasks use `initialMessageId`; report if absent, never invent it. Admission, queued/running, and missing results are not completion. `interrupt_session` is for authorized Stop.
6. `list_folders` discovers existing folder IDs, not workspace membership. Use `assign_workspace_to_folder` with `folderId` and the child's `cloudAgentSessionId`/`worktreeId` from listing or status. It files the whole worktree, including sibling chats; `folderId: null` unfiles it. Only this parent's child workspaces are movable. Folder creation, editing, deletion, and reordering are unavailable.
7. After an ambiguous mutation failure, reconcile state before retrying. Keep the same `operationKey` or follow-up `messageId` for that same submission.
8. Omitted `autoCommit` requests no automatic commit/push; follow-ups do not inherit it. Set `true` per authorized message. If publication is forbidden, set `false` and prohibit it in the prompt: `false` cannot restrict agent Git tools or cancel another prompt's request. Request PR creation in the prompt.

Authority/callbacks belong to the canonical parent (original root chat), even from
siblings. Only direct children are manageable, never unrelated sessions or the
Authority and callbacks belong to the canonical parent (original root chat), even from
siblings. A workspace callback creates a sibling chat in that parent workspace. Only direct children are manageable, never unrelated sessions or the
parent. No grandchildren.

### Examples
Expand All @@ -67,7 +67,7 @@ const child = await spawn_session({
githubRepo: 'owner/repo', model: modelId, mode: 'code',
prompt: 'Fix the bug, run focused tests, and report. Do not commit, push, or create a PR.',
operationKey: launchUuid, autoCommit: false,
callbackTarget: { url: 'session:parent' },
callbackTarget: { url: 'session:parent' }, // or { url: 'workspace:new-chat' }
});
```

Expand Down
6 changes: 5 additions & 1 deletion .specs/cloud-agent-session.md
Original file line number Diff line number Diff line change
Expand Up @@ -415,7 +415,11 @@ repository.
MUST check current canonical-parent authorization and persisted child
lineage, owner and organization. It MUST NOT manage the parent itself,
sibling chats or unrelated sessions. Child completion callbacks MUST target
the canonical workspace parent, not an arbitrary invoking sibling.
either the canonical workspace parent chat (`session:parent`) or a new chat
created in that parent's workspace (`workspace:new-chat`), never an arbitrary
invoking sibling. A workspace callback starts the new chat with the child
result as its first message and MUST be idempotent for the persisted callback
message ID.

### Watch PR (Observation-Only Pilot)

Expand Down
21 changes: 12 additions & 9 deletions apps/web/src/lib/auth/cloud-agent-authority.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -486,15 +486,18 @@ describe('scope-preserving control token exchange', () => {
});

describe('ordinary prepare schema', () => {
it('preserves the parent callback through personal and organization web prepare schemas', () => {
const input = { ...launch, callbackTarget: { url: 'session:parent' } };
expect(personalPrepareSessionNextSchema.parse(input).callbackTarget).toEqual(
input.callbackTarget
);
expect(
organizationPrepareSessionNextSchema.parse({ ...input, organizationId }).callbackTarget
).toEqual(input.callbackTarget);
});
it.each(['session:parent', 'workspace:new-chat'])(
'preserves the %s callback through personal and organization web prepare schemas',
url => {
const input = { ...launch, callbackTarget: { url } };
expect(personalPrepareSessionNextSchema.parse(input).callbackTarget).toEqual(
input.callbackTarget
);
expect(
organizationPrepareSessionNextSchema.parse({ ...input, organizationId }).callbackTarget
).toEqual(input.callbackTarget);
}
);
it('strips the retired spawn opt-in through prepare parsing', () => {
expect(
personalPrepareSessionNextSchema.parse({ ...launch, allowSubagentSessions: true })
Expand Down
4 changes: 2 additions & 2 deletions apps/web/src/routers/cloud-agent-next-schemas.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { setupCommandsSchema } from '@kilocode/cloud-agent-profile/setup-script'
import * as z from 'zod';
import { egressPolicySchema } from '@kilocode/worker-utils/egress-allowlist';
import { CLOUD_AGENT_PROMPT_MAX_LENGTH } from '@kilocode/cloud-agent-sdk/limits';
import { ParentSessionCallbackTargetSchema } from '@kilocode/worker-utils/cloud-agent-authority';
import { ParentCallbackTargetSchema } from '@kilocode/worker-utils/cloud-agent-authority';
import { CloudAgentSafeFailureSchema } from '@kilocode/worker-utils/cloud-agent-failure';
import { SandboxStatusSessionIdSchema } from '../../../../services/cloud-agent-next/src/shared/sandbox-status';
import { SESSION_ID_RE } from '../../../../services/cloud-agent-next/src/shared/protocol';
Expand Down Expand Up @@ -383,7 +383,7 @@ const requestedEnvironmentSchema = z
* initial turn fields.
*/
const PrepareSessionSharedFields = {
callbackTarget: ParentSessionCallbackTargetSchema.optional(),
callbackTarget: ParentCallbackTargetSchema.optional(),
// Repository source (mutually exclusive - must provide exactly one)
githubRepo: z
.string()
Expand Down
8 changes: 4 additions & 4 deletions apps/web/src/scripts/mcp-catalog/skill-template.md
Original file line number Diff line number Diff line change
Expand Up @@ -48,14 +48,14 @@ They do not expose API `search` or `call`.
1. Launch only when the user explicitly requests a Cloud Agent session. Children have independent sandboxes/worktrees; use local tools for this worktree.
2. Resolve the requested model or established user preference with `search_models`; copy its exact `id`. Otherwise browse with `{}` and ask the user to choose. No implicit default: `isPreferred` is a recommendation. Omit `variant` unless requested; pass only a supported variant separately. Never guess or substitute IDs. `alias_target` names a floating alias's target; discovery does not prove policy approval.
3. Call `spawn_session` with repository, self-contained task prompt, mode, exact model ID, and fresh UUID `operationKey`. Include constraints, verification, and Git/PR instructions. It starts immediately with multi-chat support. Organization and `createdOnPlatform` are inherited, not caller inputs.
4. Set `callbackTarget: { "url": "session:parent" }` for completion/failure/interruption messages; omit for none. Prefer callbacks over polling; never wait in a tool for one.
4. Set `callbackTarget: { "url": "session:parent" }` to notify the canonical parent chat, or `{ "url": "workspace:new-chat" }` to open a new chat in the parent workspace with the result as its first message. Omit for none. Prefer callbacks over polling; never wait in a tool for one.
5. `list_child_sessions` recovers IDs and `initialMessageId` when recorded; `get_session` checks status; `get_message_result` reads outcomes. Before a new authorized follow-up, call `generate_message_id({})` and copy its `messageId` into `send_message`. This generates no work. Retain it for retries; never replace it after an uncertain send. Initial tasks use `initialMessageId`; report if absent, never invent it. Admission, queued/running, and missing results are not completion. `interrupt_session` is for authorized Stop.
6. `list_folders` discovers existing folder IDs, not workspace membership. Use `assign_workspace_to_folder` with `folderId` and the child's `cloudAgentSessionId`/`worktreeId` from listing or status. It files the whole worktree, including sibling chats; `folderId: null` unfiles it. Only this parent's child workspaces are movable. Folder creation, editing, deletion, and reordering are unavailable.
7. After an ambiguous mutation failure, reconcile state before retrying. Keep the same `operationKey` or follow-up `messageId` for that same submission.
8. Omitted `autoCommit` requests no automatic commit/push; follow-ups do not inherit it. Set `true` per authorized message. If publication is forbidden, set `false` and prohibit it in the prompt: `false` cannot restrict agent Git tools or cancel another prompt's request. Request PR creation in the prompt.

Authority/callbacks belong to the canonical parent (original root chat), even from
siblings. Only direct children are manageable, never unrelated sessions or the
Authority and callbacks belong to the canonical parent (original root chat), even from
siblings. A workspace callback creates a sibling chat in that parent workspace. Only direct children are manageable, never unrelated sessions or the
parent. No grandchildren.

### Examples
Expand All @@ -67,7 +67,7 @@ const child = await spawn_session({
githubRepo: 'owner/repo', model: modelId, mode: 'code',
prompt: 'Fix the bug, run focused tests, and report. Do not commit, push, or create a PR.',
operationKey: launchUuid, autoCommit: false,
callbackTarget: { url: 'session:parent' },
callbackTarget: { url: 'session:parent' }, // or { url: 'workspace:new-chat' }
});
```

Expand Down
20 changes: 11 additions & 9 deletions packages/worker-utils/src/cloud-agent-authority.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -230,15 +230,17 @@ describe('Cloud Agent authority contract', () => {
validateCloudAgentAuthorityRequest(scope, 'cloudAgentNext.prepareSession', launch, now + 3600)
).toEqual(launch);
});
it('accepts only the parent callback destination without widening spawn authority', () => {
const request = { ...launch, callbackTarget: { url: 'session:parent' } };
expect(
validateCloudAgentAuthorityRequest(scope, 'cloudAgentNext.prepareSession', request)
).toEqual(request);
expect(cloudAgentAuthorityInputSchema(scope)).toHaveProperty(
'properties.callbackTarget.properties.url.const',
'session:parent'
);
it('accepts only the parent callback destinations without widening spawn authority', () => {
for (const url of ['session:parent', 'workspace:new-chat']) {
const request = { ...launch, callbackTarget: { url } };
expect(
validateCloudAgentAuthorityRequest(scope, 'cloudAgentNext.prepareSession', request)
).toEqual(request);
}
const schema = cloudAgentAuthorityInputSchema(scope);
expect(schema).toHaveProperty('properties.callbackTarget.anyOf');
expect(JSON.stringify(schema)).toContain('session:parent');
expect(JSON.stringify(schema)).toContain('workspace:new-chat');
});
it.each([
{ parentCloudAgentSessionId: 'workspace_sibling' },
Expand Down
20 changes: 18 additions & 2 deletions packages/worker-utils/src/cloud-agent-authority.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,26 @@ import { cloudAgentWorktreeIdSchema } from '@kilocode/session-ingest-contracts';
import { CLOUD_AGENT_NEXT_AUDIENCE, KILO_API_AUDIENCE } from './internal-service-token-audiences';

export const PARENT_SESSION_CALLBACK_URL = 'session:parent';
export const PARENT_WORKSPACE_CALLBACK_URL = 'workspace:new-chat';
export const ParentSessionCallbackTargetSchema = z
.object({ url: z.literal(PARENT_SESSION_CALLBACK_URL) })
.strict()
.describe('Notify the spawning parent session when this child finishes. Omit for no callback.');
.describe('Notify the spawning parent chat when this child finishes. Omit for no callback.');
export const ParentWorkspaceCallbackTargetSchema = z
.object({ url: z.literal(PARENT_WORKSPACE_CALLBACK_URL) })
.strict()
.describe(
'Open a new chat in the spawning parent workspace and use this child result as its first message. Omit for no callback.'
);
export const ParentCallbackTargetSchema = z
.union([ParentSessionCallbackTargetSchema, ParentWorkspaceCallbackTargetSchema])
.describe(
'Notify the spawning parent chat, or open a new chat in its workspace. Omit for no callback.'
);

export function isParentCallbackTargetUrl(url: string): boolean {
return url === PARENT_SESSION_CALLBACK_URL || url === PARENT_WORKSPACE_CALLBACK_URL;
}

export const CloudAgentSpawnProcedureSchema = z.enum([
'cloudAgentNext.prepareSession',
Expand Down Expand Up @@ -76,7 +92,7 @@ const scopedPrepareSessionSchema = z
autoCommit: z.boolean().optional(),
autoInitiate: z.literal(true),
operationKey: z.uuid().optional(),
callbackTarget: ParentSessionCallbackTargetSchema.optional(),
callbackTarget: ParentCallbackTargetSchema.optional(),
organizationId: z.string().min(1).optional(),
})
.strict();
Expand Down
11 changes: 10 additions & 1 deletion services/cloud-agent-next/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,17 @@ parent receives a queued follow-up. The parent uses its current authorization.
Delivery uses the existing five-attempt callback retry policy and one persisted
message ID per callback. Missing or inaccessible parents reject delivery.

Use `{ "callbackTarget": { "url": "workspace:new-chat" } }` instead to open a new
chat in the parent workspace. Its first message contains the same child result and
the new chat's own session ID. The new chat inherits the workspace's repository,
sandbox and agent, and can itself launch children when the workspace allows it.
The callback is idempotent for the persisted message ID, so queue retries do not
duplicate the chat.

Omit `callbackTarget` for no callback. Arbitrary session destinations are not
supported. Existing HTTP callback targets remain available to trusted integrations.
supported. Callback targets are selected by the spawning Cloud Agent and are only
available with authenticated spawn authority. Existing HTTP callback targets
remain available to trusted integrations.

### Building the Development Docker Image

Expand Down
5 changes: 4 additions & 1 deletion services/cloud-agent-next/docs/control-plane.md
Original file line number Diff line number Diff line change
Expand Up @@ -1216,7 +1216,10 @@ the canonical workspace parent, the original root chat that remains the authoriz
the existing child-session API. Every grant renewal checks that root's authorization and its owner,
organization and worktree. Deleting or revoking the root disables new grants; another sibling never
becomes the anchor. Children and parent callbacks belong to that root, not necessarily the chat
invoking MCP. The MCP transport does not send the invoking chat's identity. Built-in credential
invoking MCP. The MCP transport does not send the invoking chat's identity. A completion callback
may target that root chat (`session:parent`) or open a new sibling chat in the root's workspace
(`workspace:new-chat`); the latter uses the child result as the new chat's first message and derives
its operation and session identity from the persisted callback message ID. Built-in credential
refresh rotates the workspace MCP credential; it is not profile MCP drift and does not reconfigure
the workspace's MCP set.

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,7 @@ describe('parent session callback delivery', () => {
expect(input.turn.id).toBe(job.parentSession?.messageId);
expect(JSON.parse(input.turn.prompt.split('\n')[2])).toMatchObject({
cloudAgentSessionId: childId,
callbackSessionId: parentId,
messageId: job.payload.messageId,
status,
result: 'Child result',
Expand Down
Loading
Loading