Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
ccd381c
feat(runtime): queued quiescent mutation with quiescence waiting (#3349)
chinawch007 Aug 21, 2026
0a4c362
fix(runtime): queue permission transitions behind live execution (#3349)
chinawch007 Aug 21, 2026
21fd63b
fix(runtime): derive tool permission mode from the live boundary (#3349)
chinawch007 Aug 22, 2026
5e0db8d
feat(runtime): boundary-revision guard for backend generation reuse (…
chinawch007 Aug 22, 2026
af0f9bd
test(runtime): permission switch race matrix and seeded interleaving …
chinawch007 Aug 22, 2026
1559d7e
fix(runtime): close an admission gate instead of holding the tail whi…
chinawch007 Aug 22, 2026
3ffaf6e
fix(core): match permission modes through the structural derivation (…
chinawch007 Aug 22, 2026
63a1db3
fix(runtime-host): keep benign no-op updates working for externally i…
chinawch007 Aug 22, 2026
250385e
fix(runtime): reject a queued switch when the turn pauses on an inter…
chinawch007 Aug 22, 2026
6d12197
chore(runtime): address review P3 notes on the #3349 series
chinawch007 Aug 22, 2026
6ef9ded
test(core): adapt the legacy-execute matcher assertion to the rebase …
chinawch007 Aug 23, 2026
b1772e2
style: apply biome formatting to the #3349 series files
chinawch007 Aug 23, 2026
6498ea9
fix(runtime): apply permission narrowing on the next dispatch, not th…
chinawch007 Aug 24, 2026
61ab4a4
chore(runtime): add ASF license headers to the new #3349 test files
chinawch007 Aug 24, 2026
1428dbe
fix(runtime): drop the removed lastUsedAt field from the new test fix…
chinawch007 Aug 28, 2026
9b87fe2
fix(runtime): reject a mixed tightening while a Turn is active (#3349)
chinawch007 Aug 28, 2026
11e6a48
refactor(runtime): drop the boundary-revision backend watcher (#3349)
chinawch007 Aug 28, 2026
4484f8a
style(runtime): drop stray blank lines left by the review-fix splits …
chinawch007 Aug 28, 2026
ba731ea
test: adapt the #3349 series to main's configuration-update API (#3349)
chinawch007 Aug 28, 2026
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
57 changes: 57 additions & 0 deletions packages/core/src/__tests__/sandbox-boundary.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,8 +27,10 @@ import {
decodeExecutionBoundary,
executionBoundaryContains,
executionBoundaryDisplayMode,
executionBoundaryMatchesPermissionMode,
validateSandboxBoundaryExpansion,
} from '../sandbox-boundary.js';
import type { PermissionMode } from '../permission.js';
import {
canReadPath,
canWritePath,
Expand Down Expand Up @@ -73,6 +75,61 @@ describe('executionBoundaryDisplayMode', () => {
);
});

test('a widened read-only profile no longer matches explore (#3349)', () => {
const widened = applySandboxBoundaryExpansion(createReadOnlyPermissionProfile(), {
filesystem: { entries: [{ path: '/outside/dist', access: 'write', scope: 'subtree' }] },
});
const boundary = { kind: 'managed', profile: widened, revision: 1 } as const;

// The name stayed 'read-only' while the structure became writable: a
// no-op short-circuit keyed on this answer must not bless that divergence.
expect(executionBoundaryMatchesPermissionMode(boundary, 'explore')).toBe(false);
expect(executionBoundaryMatchesPermissionMode(boundary, 'ask')).toBe(true);
});

test('matching follows the structural derivation, not the profile name', () => {
const customReadOnly: PermissionProfileManaged = {
...createReadOnlyPermissionProfile(),
name: 'custom',
};
expect(
executionBoundaryMatchesPermissionMode(
{ kind: 'managed', profile: customReadOnly, revision: 0 },
'explore',
),
).toBe(true);
expect(
executionBoundaryMatchesPermissionMode(
{ kind: 'managed', profile: createWorkspaceWritePermissionProfile(), revision: 0 },
'ask',
),
).toBe(true);
expect(executionBoundaryMatchesPermissionMode({ kind: 'bypass', revision: 0 }, 'bypass')).toBe(
true,
);
expect(
executionBoundaryMatchesPermissionMode(
{ kind: 'managed', profile: createWorkspaceWritePermissionProfile(), revision: 0 },
'bypass',
),
).toBe(false);
});

test('a legacy persisted execute value never matches and an external boundary is not verifiable', () => {
// 'execute' left the mode vocabulary on main; a stale persisted value must
// still never match, so it always routes through a transition instead of
// being blessed as already-committed.
expect(
executionBoundaryMatchesPermissionMode(
{ kind: 'managed', profile: createWorkspaceWritePermissionProfile(), revision: 0 },
'execute' as PermissionMode,
),
).toBe(false);
expect(executionBoundaryMatchesPermissionMode({ kind: 'external', revision: 0 }, 'ask')).toBe(
false,
);
});

test('under-states danger-full-access as Auto rather than naming a mode for it', () => {
// A deliberate collapse, NOT a description of this profile: the picker
// offers two modes and no third one is being invented for a profile the
Expand Down
15 changes: 15 additions & 0 deletions packages/core/src/permission.ts
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,21 @@ export function isPermissionMode(value: unknown): value is PermissionMode {
return typeof value === 'string' && (PERMISSION_MODES as readonly string[]).includes(value);
}

/**
* The permission mode a tool-facing consumer should act on: a plan-mode
* session presents read-only authority to tools unless it is bypassed. Shared
* by the runtime-host composer (build time) and the tool runtime (dispatch
* time), so both derive the same mode from the same inputs.
*/
export function resolveCollaborationPermissionMode(input: {
readonly collaborationMode: 'agent' | 'plan';
readonly permissionMode: PermissionMode;
}): PermissionMode {
return input.collaborationMode === 'plan' && input.permissionMode !== 'bypass'
? 'explore'
: input.permissionMode;
}

/** Canonical category names use Claude SDK terminology. Pi adapter MUST
* translate Pi-native tool names into these before they reach the runtime. */
export type ToolCategory =
Expand Down
18 changes: 18 additions & 0 deletions packages/core/src/sandbox-boundary.ts
Original file line number Diff line number Diff line change
Expand Up @@ -226,6 +226,24 @@ export function executionBoundaryDisplayMode(
return readOnly ? 'explore' : 'ask';
}

/**
* Whether the durable boundary already expresses the requested permission
* mode, derived through the same structural read (#1611) as the display mode:
* a read-only-named profile widened by an approved expansion no longer reads
* as explore. Callers that short-circuit a no-op configuration update on this
* answer must consult it: comparing the header's stored `permissionMode`
* alone would bless a header/boundary divergence as already-committed.
* Legacy 'execute' never matches — forcing the transition is the safe
* direction — and an external boundary is not locally verifiable.
*/
export function executionBoundaryMatchesPermissionMode(
boundary: ExecutionBoundary,
mode: PermissionMode,
): boolean {
const displayMode = executionBoundaryDisplayMode(boundary);
return displayMode !== undefined && displayMode === mode;
}

export function createGenesisExecutionBoundary(mode: PermissionMode): ExecutionBoundary {
if (mode === 'bypass') return { kind: 'bypass', revision: 0 };
return {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -416,6 +416,79 @@ test('typed configuration rejection does not request Host drain', async () => {
assert.equal(fixture.drainRequests(), 0);
});

test('a no-op configuration update repairs a header/boundary divergence instead of blessing it', async () => {
// The header matches the requested configuration on every field, so only the
// boundary consistency check can tell a genuine no-op from a divergence that
// must be repaired through Runtime authority.
let transitions = 0;
const consistent = createFixture({
stores: {
readHeaderRecordSnapshot: async () => headerSnapshot(matchingHeader(['user-label']), 3),
readCatalogRecord: async () => catalogRecord(matchingHeader(['user-label']), 3),
readExecutionBoundary: async () => ({ kind: 'bypass', revision: 1 }),
},
manager: {
transitionSessionConfiguration: async () => {
transitions += 1;
return headerSnapshot(matchingHeader(['user-label']), 3);
},
},
});
const consistentOutcome = await consistent.coordinator.handlers['session.configuration.update'](
bypassConfigurationInput(consistent.sessionId, consistent.revision()),
context,
);
assert.equal(consistentOutcome.ok, true);
assert.equal(transitions, 0);

const divergent = createFixture({
stores: {
readHeaderRecordSnapshot: async () => headerSnapshot(matchingHeader(['user-label']), 3),
readCatalogRecord: async () => catalogRecord(matchingHeader(['user-label']), 3),
// The header says bypass while the durable boundary stays managed.
readExecutionBoundary: async () => createGenesisExecutionBoundary('ask'),
},
manager: {
transitionSessionConfiguration: async () => {
transitions += 1;
return headerSnapshot(matchingHeader(['user-label']), 3);
},
},
});
const divergentOutcome = await divergent.coordinator.handlers['session.configuration.update'](
bypassConfigurationInput(divergent.sessionId, divergent.revision()),
context,
);
assert.equal(divergentOutcome.ok, true);
assert.equal(transitions, 1);
});

test('an externally isolated session keeps benign no-op updates on the header short-circuit', async () => {
let transitions = 0;
const fixture = createFixture({
stores: {
readHeaderRecordSnapshot: async () => headerSnapshot(matchingHeader(['user-label']), 3),
readCatalogRecord: async () => catalogRecord(matchingHeader(['user-label']), 3),
readExecutionBoundary: async () => ({ kind: 'external', revision: 0 }),
},
manager: {
transitionSessionConfiguration: async () => {
transitions += 1;
return headerSnapshot(matchingHeader(['user-label']), 3);
},
},
});
const outcome = await fixture.coordinator.handlers['session.configuration.update'](
bypassConfigurationInput(fixture.sessionId, fixture.revision()),
context,
);
// The external boundary is not locally verifiable, so the header comparison
// alone decides the no-op: the store would refuse to move an externally
// isolated boundary, and a benign re-apply must not become a failure.
assert.equal(outcome.ok, true);
assert.equal(transitions, 0);
});

test('creation rejects reserved execution labels before claiming a Session identity', async () => {
let createAttempts = 0;
const fixture = createFixture({
Expand Down Expand Up @@ -1585,6 +1658,22 @@ function configurationInput(
};
}

function bypassConfigurationInput(
sessionId: string,
expectedRevision: number,
): SessionConfigurationUpdateInput {
const base = configurationInput(sessionId, expectedRevision);
return { ...base, patch: { ...base.patch, permissionMode: 'bypass' } };
}

function matchingHeader(labels: readonly string[]): SessionHeader {
return {
...sessionHeader('session-1', labels),
permissionMode: 'bypass',
orchestrationMode: 'graph',
};
}

function sessionHeader(sessionId: string, labels: readonly string[]): SessionHeader {
return {
id: sessionId,
Expand Down
11 changes: 2 additions & 9 deletions packages/runtime-host/src/server/execution-model-composition.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ import { resolveModelVisionSupport } from '@maka/core/model-metadata';
import { relayModelProfile } from '@maka/core/model-thinking';
import type { ModelCallAttempt } from '@maka/core/model-call-attempt';
import type { ModelCallCommit } from '@maka/core/agent-run';
import type { PermissionMode } from '@maka/core/permission';
import { resolveCollaborationPermissionMode } from '@maka/core/permission';
import { AiSdkBackend } from '@maka/runtime/ai-sdk-backend';
import {
buildDefaultContextBudgetPolicy,
Expand Down Expand Up @@ -475,11 +475,4 @@ class HostAiSdkBackend extends AiSdkBackend {
}
}

export function resolveCollaborationPermissionMode(input: {
readonly collaborationMode: 'agent' | 'plan';
readonly permissionMode: PermissionMode;
}): PermissionMode {
return input.collaborationMode === 'plan' && input.permissionMode !== 'bypass'
? 'explore'
: input.permissionMode;
}
export { resolveCollaborationPermissionMode } from '@maka/core/permission';
17 changes: 16 additions & 1 deletion packages/runtime-host/src/server/session-catalog-coordinator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import { isModelExplicitlyUnsupportedForChat } from '@maka/core/model-catalog';
import { thinkingVariantsForConnection } from '@maka/core/model-thinking';
import {
executionBoundaryDisplayMode,
executionBoundaryMatchesPermissionMode,
type ExecutionBoundary,
type ExecutionBoundarySummary,
} from '@maka/core/sandbox-boundary';
Expand Down Expand Up @@ -541,7 +542,21 @@ export class HostSessionCatalogCoordinator {
const clearsConnectionBlock =
input.patch.modelTarget !== undefined &&
current.header.blockedReason === 'NO_REAL_CONNECTION';
if (!clearsConnectionBlock && sessionConfigurationMatches(current.header, configuration)) {
// The boundary must match too: the header's stored permissionMode alone
// cannot bless a no-op, or a header/boundary divergence would be
// short-circuited as already-committed instead of repaired. An external
// boundary is not locally verifiable — the store refuses to move it into
// Auto or Bypass — so for those sessions the header comparison alone
// decides the no-op, keeping benign updates working.
const boundary = await this.#stores.readExecutionBoundary(input.sessionId);
const boundaryMatchesConfiguration =
boundary.kind === 'external' ||
executionBoundaryMatchesPermissionMode(boundary, configuration.permissionMode);
if (
!clearsConnectionBlock &&
boundaryMatchesConfiguration &&
sessionConfigurationMatches(current.header, configuration)
) {
return configurationSuccess({
kind: 'committed',
session: projectSessionCatalogRecord(
Expand Down
Loading