Retire ACP/Hermes/OpenCode surfaces and rename hermes_tools to mnote_agent_tools so Page AI stays on Pi Lab only. Add Chrome vault extension + extension token route, pre-release purge design, and soft-retire legacy smokes for the small-group production cut.
189 lines
6.6 KiB
JavaScript
189 lines
6.6 KiB
JavaScript
#!/usr/bin/env node
|
|
"use strict";
|
|
|
|
const assert = require("node:assert");
|
|
|
|
const BASE_URL = (process.env.MNOTE_UI_BASE_URL || "http://127.0.0.1:3000").replace(/\/+$/, "");
|
|
|
|
async function requestJson(path, init = {}) {
|
|
const response = await fetch(`${BASE_URL}${path}`, {
|
|
...init,
|
|
headers: {
|
|
"content-type": "application/json",
|
|
"x-mnote-actor-id": "user_1",
|
|
...(init.headers || {}),
|
|
},
|
|
});
|
|
const text = await response.text();
|
|
let payload = null;
|
|
try {
|
|
payload = text ? JSON.parse(text) : null;
|
|
} catch (_) {
|
|
payload = { raw: text };
|
|
}
|
|
return {
|
|
status: response.status,
|
|
headers: Object.fromEntries(response.headers.entries()),
|
|
payload,
|
|
};
|
|
}
|
|
|
|
function callPayload(sessionId, args, toolName = "mnote.onlyoffice.sheet.set_value") {
|
|
return {
|
|
toolName,
|
|
workspaceId: "ws_demo",
|
|
documentId: "doc_1",
|
|
actorId: "user_1",
|
|
sessionId,
|
|
runId: `${sessionId}-run`,
|
|
toolCallId: `${sessionId}-call`,
|
|
traceId: `${sessionId}-trace`,
|
|
idempotencyKey: `${sessionId}-idem`,
|
|
dryRun: true,
|
|
capabilityScope: ["office.write"],
|
|
args,
|
|
};
|
|
}
|
|
|
|
async function main() {
|
|
const suffix = `${Date.now()}-${Math.random().toString(36).slice(2)}`;
|
|
const bridgeSessionId = `mnote-oo-http-${suffix}`;
|
|
const bridgeToken = `token-${suffix}`;
|
|
const register = await requestJson("/api/onlyoffice/bridge/session", {
|
|
method: "POST",
|
|
body: JSON.stringify({
|
|
sessionId: bridgeSessionId,
|
|
token: bridgeToken,
|
|
editorType: "cell",
|
|
documentId: "doc_http",
|
|
assetId: "asset_http_allowed",
|
|
fileType: "xlsx",
|
|
}),
|
|
});
|
|
assert.equal(register.status, 200, JSON.stringify(register));
|
|
assert.equal(register.payload.sessionId, bridgeSessionId);
|
|
|
|
const implicit = await requestJson("/api/mnote/tools/call", {
|
|
method: "POST",
|
|
body: JSON.stringify(callPayload("sess_http_implicit", {
|
|
address: "A1",
|
|
value: "after",
|
|
})),
|
|
});
|
|
assert.equal(implicit.status, 400, JSON.stringify(implicit));
|
|
assert.equal(implicit.headers["x-error-code"], "mnote_onlyoffice_session_explicit_required");
|
|
|
|
const forbidden = await requestJson("/api/mnote/tools/call", {
|
|
method: "POST",
|
|
body: JSON.stringify(callPayload("sess_http_forbidden", {
|
|
onlyofficeSessionId: bridgeSessionId,
|
|
address: "A1",
|
|
value: "after",
|
|
aiAccessScope: {
|
|
permissionLevel: "read_write",
|
|
allowedResourceIds: ["asset_other"],
|
|
},
|
|
})),
|
|
});
|
|
assert.equal(forbidden.status, 403, JSON.stringify(forbidden));
|
|
assert.equal(forbidden.headers["x-error-code"], "mnote_onlyoffice_resource_scope_forbidden");
|
|
|
|
const missingScope = await requestJson("/api/mnote/tools/call", {
|
|
method: "POST",
|
|
body: JSON.stringify(callPayload("sess_http_missing_scope", {
|
|
onlyofficeSessionId: bridgeSessionId,
|
|
address: "A1",
|
|
value: "after",
|
|
})),
|
|
});
|
|
assert.equal(missingScope.status, 403, JSON.stringify(missingScope));
|
|
assert.equal(missingScope.headers["x-error-code"], "mnote_onlyoffice_resource_scope_required");
|
|
|
|
const allowed = await requestJson("/api/mnote/tools/call", {
|
|
method: "POST",
|
|
body: JSON.stringify(callPayload("sess_http_allowed", {
|
|
onlyofficeSessionId: bridgeSessionId,
|
|
address: "A1",
|
|
value: "after",
|
|
aiAccessScope: {
|
|
permissionLevel: "read_write",
|
|
allowedResourceIds: ["asset_http_allowed"],
|
|
},
|
|
})),
|
|
});
|
|
assert.equal(allowed.status, 200, JSON.stringify(allowed));
|
|
assert.equal(allowed.payload.result.schema, "mnote.onlyoffice.action_plan.v1");
|
|
assert.equal(allowed.payload.result.sessionId, bridgeSessionId);
|
|
assert.equal(allowed.payload.result.action, "sheet.set_value");
|
|
assert.equal(allowed.payload.audit.effect, "dry_run");
|
|
|
|
const currentImplicit = await requestJson("/api/mnote/tools/call", {
|
|
method: "POST",
|
|
body: JSON.stringify(callPayload("sess_http_current_implicit", {
|
|
aiAccessScope: {
|
|
permissionLevel: "read",
|
|
allowedResourceIds: ["asset_http_allowed"],
|
|
},
|
|
}, "mnote.onlyoffice.session.current")),
|
|
});
|
|
assert.equal(currentImplicit.status, 400, JSON.stringify(currentImplicit));
|
|
assert.equal(currentImplicit.headers["x-error-code"], "mnote_onlyoffice_session_explicit_required");
|
|
|
|
const currentForbidden = await requestJson("/api/mnote/tools/call", {
|
|
method: "POST",
|
|
body: JSON.stringify(callPayload("sess_http_current_forbidden", {
|
|
onlyofficeSessionId: bridgeSessionId,
|
|
aiAccessScope: {
|
|
permissionLevel: "read",
|
|
allowedResourceIds: ["asset_other"],
|
|
},
|
|
}, "mnote.onlyoffice.session.current")),
|
|
});
|
|
assert.equal(currentForbidden.status, 403, JSON.stringify(currentForbidden));
|
|
assert.equal(currentForbidden.headers["x-error-code"], "mnote_onlyoffice_resource_scope_forbidden");
|
|
|
|
const currentAllowed = await requestJson("/api/mnote/tools/call", {
|
|
method: "POST",
|
|
body: JSON.stringify(callPayload("sess_http_current_allowed", {
|
|
onlyofficeSessionId: bridgeSessionId,
|
|
aiAccessScope: {
|
|
permissionLevel: "read",
|
|
allowedResourceIds: ["asset_http_allowed"],
|
|
},
|
|
}, "mnote.onlyoffice.session.current")),
|
|
});
|
|
assert.equal(currentAllowed.status, 200, JSON.stringify(currentAllowed));
|
|
assert.equal(currentAllowed.payload.result.schema, "mnote.onlyoffice.session.v1");
|
|
assert.equal(currentAllowed.payload.result.session.sessionId, bridgeSessionId);
|
|
assert.equal(currentAllowed.payload.result.session.assetId, "asset_http_allowed");
|
|
|
|
console.log(JSON.stringify({
|
|
ok: true,
|
|
task: "task515-onlyoffice-live-scope-http-smoke",
|
|
baseUrl: BASE_URL,
|
|
bridgeSessionId,
|
|
implicit: { status: implicit.status, code: implicit.headers["x-error-code"] },
|
|
forbidden: { status: forbidden.status, code: forbidden.headers["x-error-code"] },
|
|
missingScope: { status: missingScope.status, code: missingScope.headers["x-error-code"] },
|
|
allowed: {
|
|
status: allowed.status,
|
|
schema: allowed.payload.result.schema,
|
|
action: allowed.payload.result.action,
|
|
audit: allowed.payload.audit.effect,
|
|
},
|
|
currentImplicit: { status: currentImplicit.status, code: currentImplicit.headers["x-error-code"] },
|
|
currentForbidden: { status: currentForbidden.status, code: currentForbidden.headers["x-error-code"] },
|
|
currentAllowed: {
|
|
status: currentAllowed.status,
|
|
schema: currentAllowed.payload.result.schema,
|
|
sessionId: currentAllowed.payload.result.session.sessionId,
|
|
assetId: currentAllowed.payload.result.session.assetId,
|
|
},
|
|
}, null, 2));
|
|
}
|
|
|
|
main().catch((error) => {
|
|
console.error(error && error.stack ? error.stack : error);
|
|
process.exit(1);
|
|
});
|