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
4 changes: 2 additions & 2 deletions plugins/agent/conversations/ui/SessionSurface.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -1136,7 +1136,7 @@ export function createConversationUi(
expectedRevision: session.revision,
title: session.title,
labels: session.labels,
permissions: { settings, revokeGrants: true },
permissions: { settings, revokeGrants: false },
});
if (changed.kind !== 'committed')
throw new UiError('Sandbox settings rejected: {reason}', {
Expand Down Expand Up @@ -1471,7 +1471,7 @@ export function createConversationUi(
access: access ?? session.permissions.access,
requestPermissions: requests ?? session.permissions.requestPermissions,
},
revokeGrants: true,
revokeGrants: false,
},
});
if (changed.kind !== 'committed')
Expand Down
2 changes: 2 additions & 0 deletions plugins/agent/native/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -175,3 +175,5 @@ compaction produces a text summary. Host freezes a single protocol operation; th
`NativeCompaction` only at stream completion. Engine and EventLog do not extract provider-options
JSON for this result. Checkpoint headers retain the producing-step reference, digest and token
estimate; baseline reads derive producer/model facts from that original request.

Disabling new requests preserves existing valid grants. Changing base access or explicitly revoking grants advances the permission boundary.
2 changes: 2 additions & 0 deletions plugins/agent/native/README.zh-CN.md
Original file line number Diff line number Diff line change
Expand Up @@ -154,3 +154,5 @@ ChatGPT 冷缓存或到期刷新在逻辑请求准备时等待,后台压缩的
订阅 Provider 仅在连接开启该选项时,在解析后的能力中选择原生压缩。关闭后仍可回放已有原生 checkpoint;下一次压缩生成文字摘要。Host 冻结一个协议操作,Responses 适配器负责追加触发命令,
并只在流完成时返回有类型的 `NativeCompaction`。Engine 和 EventLog 无需再从 provider options JSON 提取结果。
检查点头部只保留来源步骤引用、内容摘要和 token 估算,baseline 读取时从原始请求派生 producer/model。

关闭新增权限申请会保留已有有效授权;修改基础访问权限或明确撤销授权才会推进权限边界。
2 changes: 1 addition & 1 deletion plugins/agent/native/src/owner.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1101,7 +1101,7 @@ impl Owner {
serde_json::from_value(record.metadata.configuration.clone())
.map_err(|error| format!("agent_session_configuration_invalid: {error}"))?;
let policy_changed = input.permissions.as_ref().is_some_and(|change| {
change.settings != configuration.permissions || change.revoke_grants
change.settings.access != configuration.permissions.access || change.revoke_grants
});
let guard = if policy_changed {
let busy = self.runs.state().await.busy(&input.session_id)
Expand Down
12 changes: 11 additions & 1 deletion plugins/agent/native/src/owner/driver/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -414,7 +414,15 @@ impl Rig {
let methods: &[&str] = if model_test {
&["create", "models", "send", "work"]
} else {
&["create", "history", "source", "search", "list", "execution"]
&[
"create",
"history",
"source",
"search",
"list",
"execution",
"metadata",
]
};
let mut endpoints = BTreeMap::from([(
"agent".into(),
Expand Down Expand Up @@ -3356,3 +3364,5 @@ mod replay_origin_regressions {
}

mod tool_history;

mod permission_boundary;
121 changes: 121 additions & 0 deletions plugins/agent/native/src/owner/driver/tests/permission_boundary.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/

use super::*;
use maka_agent_api::contract::WorkspaceAccess;

async fn update_permissions(
rig: &Rig,
session: &str,
expected_revision: u64,
access: WorkspaceAccess,
request_permissions: bool,
revoke_grants: bool,
) -> TestResult<()> {
let mut call = rig
.client
.as_ref()
.unwrap()
.call(
"agent",
"metadata".into(),
json!({
"sessionId":session,
"expectedRevision":expected_revision,
"title":"Permission metadata regression",
"labels":[],
"permissions":{
"settings":{"access":access,"requestPermissions":request_permissions},
"revokeGrants":revoke_grants,
},
}),
Attachments::new(),
)
.await?;
call.start().await?;
assert!(matches!(call.receive().await?, Reply::Prepared { .. }));
let Reply::Mutation { reply, cleanup } =
call.confirm(json!({"test":"permission-boundary"})).await?
else {
return Err("metadata did not return a mutation receipt".into());
};
cleanup?;
let Receipt::Recorded {
outcome:
OperationOutcome::Returned {
result: OperationResult::Success { value },
},
cleanup: OperationCleanup::Confirmed,
} = &reply.receipt
else {
return Err(format!("metadata did not commit: {:?}", reply.receipt).into());
};
assert_eq!(value["kind"], "committed", "metadata mutation: {value}");
call.close().await?;
Ok(())
}

#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn permission_request_toggle_preserves_boundary_but_access_and_revoke_advance_it() {
let mut rig = Rig::new().unwrap();
let outcome = AssertUnwindSafe(tokio::time::timeout(Duration::from_secs(60), async {
let session = rig.start().await?;
let owner = rig.captured.lock().unwrap().clone().unwrap();
let mut previous = owner.mutable_session(&session).await?;
let initial = previous.configuration.permissions.clone();
let changed_access = match initial.access {
WorkspaceAccess::ReadOnly => WorkspaceAccess::WorkspaceWrite,
WorkspaceAccess::WorkspaceWrite | WorkspaceAccess::DangerFullAccess => {
WorkspaceAccess::ReadOnly
}
};
for (access, requests, revoke, preserves_basis) in [
(initial.access, !initial.request_permissions, false, true),
(initial.access, initial.request_permissions, false, true),
(changed_access, false, false, false),
(changed_access, false, true, false),
] {
update_permissions(&rig, &session, previous.revision, access, requests, revoke).await?;
let current = owner.mutable_session(&session).await?;
assert_eq!(
serde_json::to_value(&current.configuration.permissions)?,
json!({"access":access,"requestPermissions":requests}),
);
assert_eq!(
current.configuration.permission_basis()
== previous.configuration.permission_basis(),
preserves_basis,
"permission basis after access={access:?}, requests={requests}, revoke={revoke}",
);
previous = current;
}
Ok::<_, Box<dyn Error + Send + Sync>>(())
}))
.catch_unwind()
.await;
let cleanup = rig.finish().await;
assert!(
matches!(&outcome, Ok(Ok(Ok(())))),
"permission metadata regression: {outcome:?}"
);
assert!(
cleanup.is_empty(),
"permission metadata fixture cleanup: {cleanup:?}"
);
}
2 changes: 1 addition & 1 deletion products/maka/workbench/app/sessionPermissions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@ export function createSessionPermissions(host: Host, sessionId: string, control:
expectedRevision: session.revision,
title: session.title,
labels: [...session.labels],
permissions: { settings, revokeGrants: true },
permissions: { settings, revokeGrants: false },
};
pending.set(true);
try {
Expand Down
83 changes: 83 additions & 0 deletions products/maka/workbench/tests/session-permissions.test.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/

import assert from 'node:assert/strict';
import { registerHooks } from 'node:module';
import test from 'node:test';

const appRoot = new URL('../app/', import.meta.url);
const native = {
nsh: `export const signal = initial => {
let value = initial;
const read = () => value;
read.set = next => { value = next; };
return read;
};
export const cleanup = () => {};`,
locale: `export const tr = key => key;`,
};
const hooks = registerHooks({
resolve(specifier, context, next) {
const key =
context.parentURL?.startsWith(appRoot.href) && specifier === './locale'
? 'locale'
: specifier;
if (key in native)
return { url: `data:text/javascript,${encodeURIComponent(native[key])}`, shortCircuit: true };
if (
context.parentURL?.startsWith(appRoot.href) &&
specifier.startsWith('.') &&
!/\.\w+$/.test(specifier)
)
return next(`${specifier}.ts`, context);
return next(specifier, context);
},
});
const { createSessionPermissions } = await import(new URL('sessionPermissions.ts', appRoot));
hooks.deregister();

test('disabling permission requests does not request grant revocation', async () => {
const session = {
id: 'session',
revision: 4,
title: 'Permission settings',
labels: [],
permissions: { access: 'read_only', requestPermissions: true },
};
const sent = [];
const permissions = createSessionPermissions({}, session.id, async (method, input) => {
sent.push([method, input.permissions]);
return { kind: 'committed', session };
});
permissions.observe(session);

await permissions.select({
session,
settings: { ...session.permissions, requestPermissions: false },
});
assert.deepEqual(sent, [
[
'metadata',
{
settings: { access: 'read_only', requestPermissions: false },
revokeGrants: false,
},
],
]);
});