mirror of
https://github.com/paperclipai/paperclip.git
synced 2026-10-29 04:25:52 +01:00
fix(connections): preserve operator boundaries and refresh current contracts
Keep host command template administration board-only. Retain viewer restrictions, remove a duplicate fixture user, isolate harness verification, and bind the updated prompt to a new Copilot identity. Move saved setup UI and product documentation into the companion PR to keep reviews below 100 files. Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
1 parent
dfc9d612eb
commit
ff5084a8ae
22 files changed
+116
-208
No files matched your search
@@ -391,11 +391,3 @@ operation logs retain their app authorization. Workspace `_issuePrivacySources`
|
||||
operation privacy metadata protect app-record disclosure; they do not grant or deny
|
||||
filesystem execution or task-bound file browsing. Current trust and sandbox policy,
|
||||
publication receipts, binding checks, and lifecycle locks remain authoritative.
|
||||
|
||||
## Agent connection administration (2026-10-13)
|
||||
|
||||
Standard agents can configure connections through the same domain APIs used by the app, bounded by their grants and their responsible user's current authority. Fresh standard agents for default Operators can manage app/tool connections, chat/email channels, AI accounts and pools, profiles and gateways without additional grants. Existing grants are not backfilled; explicit restrictions persist. Gateway administration retains the responsible human's administrative permission and does not grant agents blanket `tools:admin`.
|
||||
|
||||
The agent performs authorized setup, reuses saved accounts and drafts, and hands off only required human authentication, consent, credential entry or identity confirmation. Saved handoffs resume through existing task continuation after current authority is rechecked. Management, tool execution and approval remain separate. Private accounts, host commands and stored credentials retain their boundaries; agent-issued gateway clients preserve issuer and responsible-user restrictions at use time.
|
||||
|
||||
The implementation, operation inventory, permission matrix and fixture qualification are recorded in [Agent API parity for connection configuration](plans/2026-10-13-connection-api-parity.md). Provider fixtures establish API behavior; they do not constitute live provider setup or an empirical agent-success claim.
|
||||
@@ -2612,11 +2612,3 @@ Company-managed [Prompt Injection Prevention](prompt-injection-prevention.md)
|
||||
optionally classifies existing tool-result boundaries through a shared OpenRouter
|
||||
connection. Settings → Connections is permanent; valid model verdicts take
|
||||
precedence and dependency failures use the original built-in detector.
|
||||
|
||||
## Agent connection administration (2026-10-13)
|
||||
|
||||
Standard agents can configure connections through the same domain APIs used by the app, bounded by their grants and their responsible user's current authority. Fresh standard agents for default Operators can manage app/tool connections, chat/email channels, AI accounts and pools, profiles and gateways without additional grants. Existing grants are not backfilled; explicit restrictions persist. Gateway administration retains the responsible human's administrative permission and does not grant agents blanket `tools:admin`.
|
||||
|
||||
The agent performs authorized setup, reuses saved accounts and drafts, and hands off only required human authentication, consent, credential entry or identity confirmation. Saved handoffs resume through existing task continuation after current authority is rechecked. Management, tool execution and approval remain separate. Private accounts, host commands and stored credentials retain their boundaries; agent-issued gateway clients preserve issuer and responsible-user restrictions at use time.
|
||||
|
||||
The implementation, operation inventory, permission matrix and fixture qualification are recorded in [Agent API parity for connection configuration](plans/2026-10-13-connection-api-parity.md). Provider fixtures establish API behavior; they do not constitute live provider setup or an empirical agent-success claim.
|
||||
@@ -864,11 +864,3 @@ Company-managed [Prompt Injection Prevention](prompt-injection-prevention.md)
|
||||
optionally classifies existing tool-result boundaries through a shared OpenRouter
|
||||
connection. Settings → Connections is permanent; valid model verdicts take
|
||||
precedence and dependency failures use the original built-in detector.
|
||||
|
||||
## Agent connection administration (2026-10-13)
|
||||
|
||||
Standard agents can configure connections through the same domain APIs used by the app, bounded by their grants and their responsible user's current authority. Fresh standard agents for default Operators can manage app/tool connections, chat/email channels, AI accounts and pools, profiles and gateways without additional grants. Existing grants are not backfilled; explicit restrictions persist. Gateway administration retains the responsible human's administrative permission and does not grant agents blanket `tools:admin`.
|
||||
|
||||
The agent performs authorized setup, reuses saved accounts and drafts, and hands off only required human authentication, consent, credential entry or identity confirmation. Saved handoffs resume through existing task continuation after current authority is rechecked. Management, tool execution and approval remain separate. Private accounts, host commands and stored credentials retain their boundaries; agent-issued gateway clients preserve issuer and responsible-user restrictions at use time.
|
||||
|
||||
The implementation, operation inventory, permission matrix and fixture qualification are recorded in [Agent API parity for connection configuration](plans/2026-10-13-connection-api-parity.md). Provider fixtures establish API behavior; they do not constitute live provider setup or an empirical agent-success claim.
|
||||
@@ -29,11 +29,3 @@ Agent-authenticated callers cannot set process adapter configuration or switch a
|
||||
The new direct grants are withheld from low trust agents and managed built-in agents. Low trust run or project policies can also deny privileged actions even if an agent has a grant. There is no permission backfill or upgrade migration for existing agents. A standard pending new hire receives the new default set when approved and activated. Invitation approval retains the set when it replaces a new agent's grants and preserves any explicitly scoped default grant. Existing scoped grants stay unchanged. A grant removed by an operator stays removed; ordinary updates and startup do not reapply it.
|
||||
|
||||
The standard agent configuration default supports the agent setup in the linked runner task. Enabling a warm runtime can still depend on the target agent's adapter and runtime configuration, provider availability, and any responsible-user or approval checks.
|
||||
|
||||
## Connection administration (2026-10-13)
|
||||
|
||||
The five default tool grants are usable through the existing connection APIs and default native `search_api`/`call_api`. A fresh standard agent for a normally provisioned Operator can configure app/tool, chat/email and AI connections, profiles and gateways without additional agent grants. Gateway management uses the agent's `tools:manage_connections` grant and still requires `tools:admin` on the responsible human. It does not grant blanket agent administration.
|
||||
|
||||
Connection management always intersects the current human's authority, including in responsible-user shadow mode. Personal self-service remains available where the human setup surface permits it; sharing and assignments require authority over their targets. Current run/work-mode/trust restrictions, scoped grants and explicit grant removal remain effective. No existing-agent permissions are backfilled. Agent-issued gateway clients preserve this delegation at every use, even after the setup run ends.
|
||||
|
||||
See [the authorization and qualification record](plans/2026-10-13-connection-api-parity.md) and the operation-reviewed [agent access maps](../maps/agent-access-map/connection-setup.md). Human consent, credential entry and identity confirmation remain bounded handoffs to saved setup.
|
||||
@@ -1,107 +0,0 @@
|
||||
# Agent API parity for connection configuration
|
||||
|
||||
Dated 2026-10-13. Implementation and qualification record for the connection-management change based on `e43af9770a36ff92e14cf0e31ee16fa238310f76`.
|
||||
|
||||
## Outcome
|
||||
|
||||
A newly created standard agent can administer connections using its existing default grants and its responsible user's authority. The default Operator has the human permissions needed for these operations. API reachability includes app/tool connections, chat/email endpoints, GitHub and Slack setup, AI accounts and pools, profiles, named gateways and gateway clients. Human authentication and trusted-host operations retain their explicit boundaries.
|
||||
|
||||
No agent grant backfill is performed. An existing missing or scoped grant remains missing or scoped. An operator can explicitly change those grants through the existing permission-management surface. Agents do not acquire `tools:admin` by default and cannot edit their own principal permissions or approve their own governed requests.
|
||||
|
||||
## Authorization contract
|
||||
|
||||
`server/src/services/connection-management.ts` resolves authenticated management context and invokes the existing authorization evaluator. `server/src/routes/connection-management-authz.ts` resolves route resources and all referenced targets, then supplies that context to services. Capability reporting calls the same evaluator; every mutation checks authority again.
|
||||
|
||||
| Operation | Agent grant | Responsible-user ceiling |
|
||||
| --- | --- | --- |
|
||||
| Configure connections, channels and AI accounts | `tools:manage_connections` | Existing personal/creator authority or connection management |
|
||||
| Profiles, assignments and action policies | `tools:manage_profiles` | Corresponding profile and target-agent authority; changes to connection audience also require connection management |
|
||||
| Named gateways and client tokens | `tools:manage_connections` | `tools:admin`; bindings also require profile and target authority |
|
||||
| Audit | `tools:view_audit` | Audit and resource visibility |
|
||||
| Start/stop runtimes | `tools:manage_runtime` | Runtime and resource authority |
|
||||
| Test actions | `tools:use` | Credential access, target-agent authority and ordinary tool/action policy |
|
||||
|
||||
The acting principal, responsible user, credential owner and target agent remain distinct. Personal credentials belong to the responsible human. Activity records identify the actual agent, responsible user, run or API key, resource and authorization reason. API parameters cannot select a responsible user or forge ownership attribution.
|
||||
|
||||
Agent keys resolve their saved responsible user. Run JWTs require current run identity and assignment. Both fail closed on absent user context, inactive membership, foreign companies, unavailable personal credentials, invalid scopes or low trust. Connection authorization enforces the human ceiling even when general responsible-user authorization runs in shadow mode. `skill_test` and `task_bridge` cannot administer connections. Ask and Plan contexts cannot mutate configuration. Connection/profile/gateway/provider/purpose/operation grant scopes apply to resolved resources, including plural policy selectors and affected targets.
|
||||
|
||||
Services recheck management context after asynchronous provider work before sensitive persistence. Existing revisions, transaction locks, credential leases, vault storage, duplicate prevention and cleanup stay authoritative. Reads filter private accounts, referenced profile/pool resources and task activity. Configuration authority is separate from use: an allowed policy change does not bypass Off/Ask first/Allowed execution policy.
|
||||
|
||||
Binding and audience authorization checks field presence, including `null`, `false` and empty arrays. Clearing an assignment or reducing an audience remains a profile-management operation after an explicit grant revocation. Gateway binding changes check both the saved and replacement profile/project/task contexts; reauthorization reloads the saved side after asynchronous work. Referenced resources require each applicable connection and profile permission, including the replacement side of a project binding.
|
||||
|
||||
## Saved authentication and delegation
|
||||
|
||||
Agent setup responses retain their browser fields and add `requiredUserAction` where a human step is needed. It identifies the addressed user, saved resource, setup destination, reason and continuation. Chat handoffs reuse the saved endpoint and existing connection-intent delivery. AI login handoffs reopen the saved sign-in intent in the existing credential UI. Completion is based on server state; the UI does not ask a human to certify setup success.
|
||||
|
||||
Generic app OAuth returns the saved setup destination without creating agent-bound provider state; the human browser starts the existing session-bound authorization flow. Its `expiresAt` is null until that human step creates an expiring provider session. Slack/GitHub provider callbacks preserve human subject and state checks. Agent-created GitHub setup binds its browser session when the addressed human opens it. Identity callbacks validate the current human and session before consuming state, and revalidate authority before saving identity. Wrong-user callbacks leave the rightful user's flow intact. Continuation rechecks task state and current authority after a yielding run; cancelled tasks and revoked access cannot be revived by a late provider result.
|
||||
|
||||
Agent-issued durable gateway clients store `responsible_user_id` alongside `created_by_agent_id`. Token use reevaluates issuer/user authority, gateway/profile scope, target constraints, credential access and action restrictions. The responsible-user marker remains after deletion clears the issuer foreign key, so an orphaned delegated token fails closed instead of becoming a system client. It does not inherit the lifetime of the setup run. Heartbeat tokens remain bound to their authenticated run. Legacy agent-issued clients without a responsible user fail closed; existing board-issued clients retain their behavior. Migration `0378_slim_nehzno.sql` is additive and does not infer legacy identity or modify agent grants.
|
||||
|
||||
## Discovery and guidance
|
||||
|
||||
OpenAPI carries explicit REST transport and connection-management authorization metadata. JSON authorization initiation and status are callable through default `search_api`/`call_api`; OAuth callbacks, redirects, MCP and WebSocket sessions retain their protocols. There is no new administration tool family.
|
||||
|
||||
The shared connection guidance is delivered by the runtime prompt, search tools and existing adapter/CLI consumers. It tells agents to distinguish tool/channel/AI purposes, configure what the user requested when authorized, suggest relevant missing connections, reuse saved drafts and hand off only missing decisions, credentials, consent or authority. It prohibits silently relaxing policy after failed execution and distinguishes saved, authenticated, usable and verified states. Native API setup secrets and gateway credentials use protected replay receipts and redacted history.
|
||||
|
||||
## Operation inventory
|
||||
|
||||
The existing maps are the checklist, not a second runtime registry:
|
||||
|
||||
| Inventory | Coverage |
|
||||
| --- | --- |
|
||||
| [Connection setup](../../maps/agent-access-map/connection-setup.md) | Catalog/method discovery, preflight, drafts, authentication, connection intents and host boundaries |
|
||||
| [App permissions](../../maps/agent-access-map/app-permissions.md) | Account lifecycle, installations/grants, policy, catalog, action tests and activity |
|
||||
| [Chat channels](../../maps/agent-access-map/chat-channels.md) | Endpoint/provider setup, settings, defaults, destination access, GitHub/Slack registration and identity, email, tests and lifecycle |
|
||||
| [Gateways and profiles](../../maps/agent-access-map/gateways-profiles.md) | Profiles/bindings, gateway configuration, client tokens, runtime controls, protocol and audit |
|
||||
| [Model accounts](../../maps/agent-access-map/runs-adapters.md#model-accounts) | AI discovery, key/login setup, reconnect/defaults, managed agent bindings, pools and lifecycle |
|
||||
|
||||
The route inventory was reconciled against the production `tools`, `chatEndpoints`, `email`, `ai-connections`, `ai-connection-pools` and relevant `agents` UI API clients. Human-only operations remain individually classified: browser callback/identity confirmation and credential entry, principal approvals, arbitrary local configuration import/host-template authoring, cloud instance enrollment, and manual chat publication/replay that does not run through the governed agent tool path. Approved template use and ordinary authorized remote configuration remain delegable. Catalog/provider availability gates still apply.
|
||||
|
||||
## Qualification
|
||||
|
||||
`server/src/__tests__/connection-management.integration.test.ts` runs the assembled server, production bearer authentication, real database/vault persistence and deterministic provider responses. It creates each standard agent with an actual human-authenticated `POST /companies/:companyId/agents`; it provisions the human using normal Operator role defaults. It adds no special agent grants and does not inject `req.actor`. Only fixture sign-in throttling is disabled. GitHub/Slack/OpenAI/AgentMail responses are fixtures, not live provider proof.
|
||||
|
||||
The suite exercises:
|
||||
|
||||
- Fresh default app connections, profiles, chat settings, AI account/binding/pool and named gateway/token creation; no default `tools:admin` grant.
|
||||
- Actual persistent API keys and run JWTs, native discovery/calls, and delivery of the new built-in guidance.
|
||||
- Correct human ownership and actual-agent audit attribution, including personal grant delegation and revocation; persisted settings and optimistic revision conflicts.
|
||||
- Slack configured credentials and test messages under ordinary action policy; repeated request IDs do not resend.
|
||||
- GitHub app import, repository/verification configuration and real-session identity handoff; wrong-user and replay rejection.
|
||||
- Saved chat handoff reuse and existing task continuation; late credential promotion after yield, cancellation and user revocation.
|
||||
- User restrictions with legitimate personal self-service, missing/scoped agent grants, foreign companies/accounts, forged identity, invalid responsible users, bridge keys, low trust and work-mode restrictions.
|
||||
- Durable gateway use after the setup run finishes, purpose-scoped grants, denial after membership or issuer-grant revocation and in the persisted cleared-issuer state, encrypted native token receipts, redacted history and retry without a second token.
|
||||
|
||||
Focused neighboring tests cover existing human routes, callback behavior, provider setup, device/setup-token sign-in, native API policy, secret redaction, saved setup UI and connection-intent behavior. Maps distinguish individually executed fixture operations from source-reviewed operations. Scripted evidence proves reachability and behavior, not an empirical LLM success rate.
|
||||
|
||||
Required final checks are `node scripts/check-maps.mjs`, `pnpm -r typecheck`, `pnpm test:run`, `pnpm build`, database migration validation and UI token gates. The final handoff records their actual results. Live provider setup, a new chatbot skill and redesigned setup workflows are outside this delivery.
|
||||
|
||||
## Historical validation record (2026-10-13)
|
||||
|
||||
- Focused connection, AI hiring, native API and OpenAPI suites: **2,533 passed**, including **16 assembled-server acceptance scenarios** using production authentication and default agent creation.
|
||||
- Final fresh-process connection qualification: **618 passed across five suites**, including **all 18 assembled-server acceptance scenarios**, 418 tool-access tests, 87 gateway acceptance tests, 73 authorization tests and 22 decision-model tests. This covers binding/audience revocation, both sides of profile/project scope changes, current membership authority and gateway-token ownership metadata.
|
||||
- Chat compatibility regression selection: **49 passed** (Slack tests, resource auditing, manager boundaries and Teams activity/projections).
|
||||
- Saved setup UI, canonical guidance and native history redaction: passed in the neighboring focused run; its three outdated API transport/security assertions were corrected and included in the 2,533-test passing run.
|
||||
- `pnpm -r typecheck`: passed.
|
||||
- `pnpm build`: passed.
|
||||
- Database migration numbering/safety validation: passed through typecheck/build; existing historical safety findings remain covered by the repository baseline.
|
||||
- `node scripts/check-maps.mjs`: passed, 35 areas / 176 capability IDs; structural consistency only.
|
||||
- UI token gates: passed.
|
||||
- Broad UI run: 8,210 passed / 5 failed under contention; both failing suites passed serially (82 tests).
|
||||
- Shared contracts and catalog: 941 passed. Adapter utilities: 1,740 passed / 5 failed under contention; all five failing suites passed serially (217 tests).
|
||||
- Remaining source-only adapter/plugin projects: 2,087 passed / 1 macOS temporary-path assertion failed; that suite passed with the repository runner’s canonical temporary directory (14 tests).
|
||||
- Serialized route/auth suites without embedded PostgreSQL: 1,849 passed / 22 failures in two empty-database fixtures; after adding the resource/membership fixture rows, both suites passed (25 tests). Their authentication, isolation and provider-error assertions remain exercised.
|
||||
- Email address/provider regression suite: 10 passed after supplying resource and membership rows to its old empty-database fixture. HTTP log redaction: 66 passed in a fresh process after a sweep timeout.
|
||||
- CLI suites without embedded PostgreSQL: 462 passed across 63 files. The three database-backed suites produced 62 passes and three embedded-PostgreSQL initialization failures in `worktree.test.ts`. The host was at its 32/32 shared-memory segment limit; these worktree tests need additional concurrent clusters. Other chats' databases and system limits were not modified.
|
||||
- Database package: 194 passed across 50 files.
|
||||
- Database-backed serialized routes: 1,208 passed / two failed / 73 skipped. Seven suites hit their 20-second database startup limit; the failures were an outdated AI-pool role fixture and a routine test timeout. All affected suites passed fresh-process reruns.
|
||||
- Final isolated gateway/service/route reruns: **320 passed across 13 suites**, including the startup failures, skill cleanup deadlock, AI-pool membership fixture and routine timeout. The stale-lock route suite initially skipped its 14 tests after a failed database availability probe; a final isolated retry executed and passed all 14.
|
||||
- Final assembled-server acceptance after the gateway scope and orphaned-token fixes: **18 passed**. The orphaned-token case models the token foreign key's persisted `ON DELETE SET NULL` state without deleting protected historical audit references; token use still traverses the real public gateway endpoint.
|
||||
- Final workspace typecheck/build, migration checks, maps, token gates and `git diff --check`: passed. The generated migration retains exactly the five newest schema snapshots.
|
||||
- `pnpm test:run`: the general server sweep finished with 19,282 passed / 15 failed / 381 skipped across 914 files (898 passed, 10 failed, 6 skipped). Four suites could not start their databases; other failures included two gateway tests loaded against earlier transformed authorization code, outdated role/resource fixtures, a cleanup deadlock, an HTTP timeout and a socket hang-up. All reported server failures and blocked startup suites subsequently passed fresh reruns. Workspace and serialized groups were executed separately after the runner's fail-fast exit. The original sweep is not represented as a clean pass.
|
||||
|
||||
Qualification is complete for the connection feature and its provider fixtures. The repository-wide test result remains **not fully green** because the three CLI worktree tests could not initialize their additional PostgreSQL clusters at the host's shared-memory limit. They require rerunning on a host with spare PostgreSQL capacity. Live provider verification remains outside this delivery.
|
||||
|
||||
## PR qualification (2026-10-26)
|
||||
|
||||
The implementation was rebased onto `a1f77821b9cd5c9c846fdf7a0fa5cc620ad2b6bb`. Connection-service changes were ported into the current extracted modules. The historical checks above describe the October 13 snapshot; fresh PR qualification is recorded separately below.
|
||||
@@ -32,12 +32,12 @@
|
||||
"requiresProviderPolicy": true
|
||||
},
|
||||
"copilot": {
|
||||
"agentProfileVersion": 59,
|
||||
"agentProfileVersion": 60,
|
||||
"agentServerPackage": "@github/copilot",
|
||||
"agentServerVersion": "1.0.88",
|
||||
"agentRuntimePackage": null,
|
||||
"agentRuntimeVersion": null,
|
||||
"commandDigest": "sha256:2f39ffe54a6af832d044b5098c31df9e08d4d27aee0ac675ba4b0c9da7445c40",
|
||||
"commandDigest": "sha256:47e7c5a10f2d23159d9675f4b4b2d89bfb7156aaae8a366158c3174780cf2a9e",
|
||||
"requiresProviderPolicy": true
|
||||
},
|
||||
"claude": {
|
||||
|
||||
@@ -47,7 +47,7 @@ pub(crate) fn acpx_release_profile(agent: &str) -> Option<AcpxReleaseProfile> {
|
||||
agent_runtime_package: None,
|
||||
agent_runtime_version: None,
|
||||
command_digest:
|
||||
"sha256:2f39ffe54a6af832d044b5098c31df9e08d4d27aee0ac675ba4b0c9da7445c40",
|
||||
"sha256:47e7c5a10f2d23159d9675f4b4b2d89bfb7156aaae8a366158c3174780cf2a9e",
|
||||
requires_provider_policy: true,
|
||||
},
|
||||
"claude" => AcpxReleaseProfile {
|
||||
|
||||
@@ -7,9 +7,9 @@ import { fileURLToPath } from "node:url";
|
||||
import { resolve, dirname } from "node:path";
|
||||
|
||||
const root = resolve(dirname(fileURLToPath(import.meta.url)), "..");
|
||||
const fixture = resolve(root, "test/fixtures/copilot-profile-v59-identity.json");
|
||||
const fixture = resolve(root, "test/fixtures/copilot-profile-v60-identity.json");
|
||||
const historical = JSON.parse(readFileSync(resolve(root, "test/fixtures/copilot-profile-v12-identity.json"), "utf8"));
|
||||
const declaration = { ...historical.declaration, agentProfileVersion: 59 };
|
||||
const declaration = { ...historical.declaration, agentProfileVersion: 60 };
|
||||
delete declaration.permissionIdentitySourceSha256;
|
||||
const sources = {
|
||||
displayEventSourceSha256: "src/drivers/acpx/copilot-events.ts",
|
||||
|
||||
@@ -87,9 +87,19 @@ vi.mock("./installation-integrity.js", () => ({ verifyNativeAcpxInstallation: vi
|
||||
|
||||
describe("Copilot build-owned installation", () => {
|
||||
it("admits the current profile with environment launch preparation", () => {
|
||||
expect(QUALIFIED_ACPX_PROFILES.copilot.agentProfileVersion).toBe(59);
|
||||
expect(QUALIFIED_ACPX_PROFILES.copilot.agentProfileVersion).toBe(60);
|
||||
expect(QUALIFIED_ACPX_PROFILES.copilot.qualificationStatus).toBeUndefined();
|
||||
expect(QUALIFIED_ACPX_PROFILES.copilot.commandDigest).toBe("sha256:2f39ffe54a6af832d044b5098c31df9e08d4d27aee0ac675ba4b0c9da7445c40");
|
||||
expect(QUALIFIED_ACPX_PROFILES.copilot.commandDigest).toBe("sha256:47e7c5a10f2d23159d9675f4b4b2d89bfb7156aaae8a366158c3174780cf2a9e");
|
||||
});
|
||||
|
||||
it("preserves v59 while binding delegated connection guidance in v60", () => {
|
||||
const prior = JSON.parse(readFileSync(new URL("../../../test/fixtures/copilot-profile-v59-identity.json", import.meta.url), "utf8"));
|
||||
const current = JSON.parse(readFileSync(new URL("../../../test/fixtures/copilot-profile-v60-identity.json", import.meta.url), "utf8"));
|
||||
expect(Object.keys(current.declaration).filter(key => current.declaration[key] !== prior.declaration[key]).sort()).toEqual([
|
||||
"agentProfileVersion", "nativeRuntimePromptsSourceSha256", "profileCompatibilitySourceSha256",
|
||||
]);
|
||||
expect(isSupportedAcpxProfileVersion("copilot", 59)).toBe(true);
|
||||
expect(current.commandDigest).not.toBe(prior.commandDigest);
|
||||
});
|
||||
|
||||
it("preserves v58 while binding the separate remote process reconnect policy in v59", () => {
|
||||
|
||||
@@ -48,12 +48,12 @@ export const QUALIFIED_ACPX_PROFILE_DATA = {
|
||||
"protocolVersion": 1,
|
||||
"acpxVersion": "0.13.1",
|
||||
"agent": "copilot",
|
||||
"agentProfileVersion": 59,
|
||||
"agentProfileVersion": 60,
|
||||
"agentServerPackage": "@github/copilot",
|
||||
"agentServerVersion": "1.0.88",
|
||||
"agentRuntimePackage": null,
|
||||
"agentRuntimeVersion": null,
|
||||
"commandDigest": "sha256:2f39ffe54a6af832d044b5098c31df9e08d4d27aee0ac675ba4b0c9da7445c40",
|
||||
"commandDigest": "sha256:47e7c5a10f2d23159d9675f4b4b2d89bfb7156aaae8a366158c3174780cf2a9e",
|
||||
"permissionPolicy": "interactive"
|
||||
},
|
||||
"claude": {
|
||||
|
||||
@@ -2,13 +2,13 @@ import { QUALIFIED_ACPX_PROFILE_DATA } from "./generated-profiles.js";
|
||||
|
||||
/** Decodable historical profile revisions, not permission to launch them.
|
||||
* Exact current profile/package/digest admission is checked separately. */
|
||||
type HistoricalAcpxProfileVersion = 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58;
|
||||
type HistoricalAcpxProfileVersion = 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59;
|
||||
type CurrentAcpxProfileVersion = typeof QUALIFIED_ACPX_PROFILE_DATA[keyof typeof QUALIFIED_ACPX_PROFILE_DATA]["agentProfileVersion"];
|
||||
export type AcpxProfileVersion = HistoricalAcpxProfileVersion | CurrentAcpxProfileVersion;
|
||||
|
||||
const historicalVersions: Readonly<Record<string, readonly HistoricalAcpxProfileVersion[]>> = {
|
||||
pi: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28], claude: [1, 2, 3, 4, 5], codex: [1, 2, 3, 4, 5],
|
||||
grok: [1, 2, 3, 4, 5], copilot: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58],
|
||||
grok: [1, 2, 3, 4, 5], copilot: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59],
|
||||
cursor: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15],
|
||||
};
|
||||
|
||||
|
||||
@@ -0,0 +1,85 @@
|
||||
{
|
||||
"commandDigest": "sha256:47e7c5a10f2d23159d9675f4b4b2d89bfb7156aaae8a366158c3174780cf2a9e",
|
||||
"declaration": {
|
||||
"schema": "paperclip.acpx_profile_declaration.v1",
|
||||
"agent": "copilot",
|
||||
"agentProfileVersion": 60,
|
||||
"acpxVersion": "0.13.1",
|
||||
"agentServerVersion": "1.0.88",
|
||||
"protocolPolicyRevision": "copilot-agent-manual-v1",
|
||||
"acpxPatchSha256": "00d6af17216cb0cad902b48633eb19bf0ec6b05837a984cb24661fca593f09c1",
|
||||
"policySha256": "a4be69be43db84052560420a3ca546a665c9165573c24d543eb33d144ba138a3",
|
||||
"distributionSourceSha256": "c0a65e247fc47b263bd369a3d5ab505c4b323d4e6493be8a9ecbeb0e9dbb9107",
|
||||
"agentFilesBinding": "registered-runtime-context-v1",
|
||||
"systemInstructionDelivery": "COPILOT_HOME/copilot-instructions.md:replace-under-lifetime-lease-before-launch:v1",
|
||||
"sharedRuntimeContract": "paperclip.acpx-runtime-contract.v1",
|
||||
"innerDistributionSourceSha256": "9bb08347eed01f1ab44e065c8b06ead9be3a25973d6e6329dc4c0a099096279a",
|
||||
"upstreamAppSha256": "7b48282a19b5b0814a0c96ad5e3a125173d792cc55f9525b2effea962f6063c0",
|
||||
"patchedAppSha256": "61f8eebe0c30cc03b311c6818b2acb158a6d16ca58c8bfac5ebd42db095c2679",
|
||||
"messageIdentityContract": "copilot-native-message-id-v1",
|
||||
"ownedDistributionDelivery": "COPILOT_CLI_DIST_DIR:lease-owned-guarded-inner-distribution:v1",
|
||||
"permissionContextContract": "copilot-edit-permission-context-v1",
|
||||
"permissionContextSourceSha256": "de2d38d702b3c574161af0c0c04790fb6f6347916eb3916ce7a131a8c229121b",
|
||||
"permissionAdapterSourceSha256": "ee5929ed2ee647ad0369465dee9d51a00b2dd28e1ff735b43e969ddc9c164668",
|
||||
"permissionLocationsSourceSha256": "f39a81da395b6e23aa2a1a45572a28846e5e81bfd3d764d9a456f74a79c752e9",
|
||||
"permissionRedactionSourceSha256": "ad6770c69269087951790e2e87e22ac088e3d432769adf17ebc865ed512e6c4f",
|
||||
"permissionClassifierSourceSha256": "1161eabd82e26133e3b3db3ab567f65d52808b03f176e93497e02c8330349fb4",
|
||||
"semanticToolReceiptContract": "paperclip.semantic_tool_receipt.v2",
|
||||
"semanticReceiptSourceSha256": "b2795726b2234a663fec1a6ca11f0551ae69882fe9e82aa39686e952d036e791",
|
||||
"semanticBridgeSourceSha256": "8764b8c9032fdc7d1415b01e75a9fc57c550e7a5e11da4bed7d9100082872ae5",
|
||||
"toolEvidenceSourceSha256": "6eef5039afc75bb851eaf9cd82a6f099e4bd398ca2deb59dd66f5b2a81a7a706",
|
||||
"semanticNormalizedInputContract": "validated-forwarded-input-commit-v1",
|
||||
"semanticSidecarSourceSha256": "17361d1782c0dcebb2b7972635f45c890f2a4081f8bc0ef2d2276550e0b91629",
|
||||
"semanticDirectDriverSourceSha256": "ff572638f2cb5d9f8249ff8e2f412311c1e3af209ef0bf7e5ea9f10a21d91103",
|
||||
"semanticValidationSourceSha256": "02fbb92681c78c5de5d2f1aae25aabc25a0e5df6b0ff4baccac652192a8da975",
|
||||
"semanticNormalizationSourceSha256": "8b628b82e8eacc91cdcc6fdca2210ba4cf282f01e9c6f74e95ff271655b5047e",
|
||||
"semanticCompletionContractSourceSha256": "3343df6362252d44d12b079ff78c8f6539d1544f3ce5ec36ff36db70ab0fac31",
|
||||
"semanticValidatorsSourceSha256": "f22d9caf2035b600abc1a6625ae23ca97f1409d7e29ed5b7264bb8f9784c7dc9",
|
||||
"semanticSchemaBundleSourceSha256": "016391a087cc217aafed4ba1f4efafc6e4881a7525952dce824b7f4d24530603",
|
||||
"semanticSidecarProtocolSourceSha256": "03c0c95b900f9dc18f29f6be27b8c79bc0bcab29780190d46a74e95328081236",
|
||||
"semanticSidecarReceiptCommitContract": "correlated-tool-resolve-v1",
|
||||
"displayEventSourceSha256": "d72f9a6035c247bc6a42c067c05753076c54322c4b58984e93922f3325dc6877",
|
||||
"displayProjectionSourceSha256": "75952606b26dd93bbc582c1f0a4424487377272ea745fb3a27ceae031fd5dc7e",
|
||||
"configuredEnvironmentSourceSha256": "5c9008d121979a3a8987e6b8a5752172eafdc4a2b5d725a73f3bbb8fb83be8ff",
|
||||
"configuredEnvironmentRustSourceSha256": "85c87fae62b8a9377e4f0a4c00302c7519fae4397b1c48253f8f2b7ba7faf994",
|
||||
"profileActivitySourceSha256": "4fdb6b0f9125fd8372ba6f27945fa5c8a3d3b2ec891df544092e3dc7a75d5282",
|
||||
"profileExtensionsSourceSha256": "a80ddc95eb4505095bfdbbf85c758199949ab20be9c84b34c77f762eb7180571",
|
||||
"profileInstallationSourceSha256": "3e61c4cdd4b78e38fcb16eaadae4f1fd65ce3c7127184ea7a80948c8e5d77f88",
|
||||
"attachmentPolicySourceSha256": "4f7638b7cc64e18f6a4ba33fd46de2b3d49225e32aeab41f5c695b268fa4f8e2",
|
||||
"singleReadEvidenceSourceSha256": "4069393405089bde613cecec04f5123c426594811da4c668bd96f0e7e283550f",
|
||||
"nativeSessionRuntimeSourceSha256": "1fe6fa9e6b96d84d70432b85a57c473f833606d4a521563d5022b4d3c41576ce",
|
||||
"nativeRuntimeContextSourceSha256": "71607f80dbb8dc5cc6c123d0b8f3995d6c5407277695a8c82cbedc90fb8f4842",
|
||||
"nativeRuntimePromptsSourceSha256": "fff5755dba9b7dce8a5f10ff6ac6d5ed06b92ed079ce1e0a44ac72f2ce7869c1",
|
||||
"nativeSessionContractSourceSha256": "22a8c96ce41ba4f03e976fc37f0cc7c5d05cee99fa789941da31ee1dab440b90",
|
||||
"harnessSessionContractSourceSha256": "03f4cd7f76f34f6d82e009b66aa51f18f6e23dc235b660778060e35b6ea07412",
|
||||
"harnessBackendSourceSha256": "dc827e281ae6731545b166ee4d709a4f67f594a556dcbe63c70fe8f1aca26249",
|
||||
"harnessSessionSourceSha256": "81361733e86b56dd5a66415aefc6f72e69be24acc8d3417370154ebb7fef6e94",
|
||||
"transportContractSourceSha256": "03fca93d36a89b0cc0917335e293ad097fdf04356d21b307827bb0ea4c13f523",
|
||||
"nativeBackendSourceSha256": "972c08e6e320d1384221e180391fe817ea1ba632db687201e457d64e68ef4360",
|
||||
"nativeBackendFactorySourceSha256": "7e431c926b76d53c4c08e3a4105353dcd34bd940f6588a1a68f79631f6feeb44",
|
||||
"nativeInstructionsSourceSha256": "9173ebccf09cb56a781d2bf5c7eb27fdafb13d3450d99342c6aff86b42d6b43a",
|
||||
"providerLaunchSourceSha256": "d4f4a69baeeb2f3b2f350840f81f711e40f08588c28ea28039748dde90302a51",
|
||||
"launchPreparationStoreSourceSha256": "ff6025568bbc0aa6ce545772d73dfca2d3da226740bd3a7b386b069e76cff296",
|
||||
"runtimeRequestBridgeSourceSha256": "7497083b6cf1fc0268187aff948352f3bce6e054ac6e845005846473efdaff15",
|
||||
"runtimeRequestFacadeSourceSha256": "d0ed28e0c5ddad34db8f437e704c8a755eb0b47f27e08e44185f66d1d2205295",
|
||||
"runtimeRequestNotificationSourceSha256": "6196c6175f11c69295dc00fc773201f4fdeca26c500653870253c43b81e65211",
|
||||
"guardedInstallationSourceSha256": "56a177d3e702914818dca4ca50e061f0d25f988bfc4c0582c36ce23c1696d82f",
|
||||
"nativeExecutionSourceSha256": "fc7c6568feb0d8d709e9baa51839420c9b90336b4865a9b8405aba06e4d1e9c4",
|
||||
"profileCompatibilitySourceSha256": "8503e7f2f8175e413b42e85a9ff259a00f2278cd966bbeafc9e9e85417cf7dbb",
|
||||
"installationSourceSha256": "a1c7c6f103b898216973e77ffbac0ef7572e250aa574d1a4065270e9dfa0dc2d",
|
||||
"assetOwnershipSourceSha256": "1785e3f31e5dad5a13b53e25d8cf9482a9bd89a27b6c127830aed6b81acc605c",
|
||||
"assetInstallerSourceSha256": "d3bd9514e5920d0cc3567aa50487fb534c57a6f66b8da0d3f22c5f6e499c841f",
|
||||
"credentialBindingSourceSha256": "7586d44b47c83395498dc8806287cd3538ef58177ba7c5949f5766ed105a0161",
|
||||
"sidecarErrorSourceSha256": "b09af4577f8bf42eee7036b2ad9842202cfc763e72b53c3287f82c6090cdad80",
|
||||
"profileSourceSha256": "f5538b3dbefa7634a332b87d5ededcefb56aa7e5790cede1c7a0d948a548a456",
|
||||
"metadataProbeSourceSha256": "be4c8416edd27061109bc1f33d0c26071b4facbb3f651beaae6b3c0275cce727",
|
||||
"permissionToolIdentitySourceSha256": "770b47305e571a81f344883057d0e4a482a1cd59ac77d045836593a8e739deb1",
|
||||
"semanticContinuationSourceSha256": "d248e47a0fe7f2536d1e64cea0b0b410f34104800cd643d995b4473104ecc8e0",
|
||||
"runtimeHostSourceSha256": "94a73749267f5a80b5df19413a65b4b9263c00a9bf7e98fda00f00b8f9fd32ec",
|
||||
"runtimeAdapterSourceSha256": "e4c764c2c3c7dd655a965b39472bd1ad8d577d1e7258b25594b171c9429752b8",
|
||||
"runtimeSandboxSourceSha256": "92dfa53f5a3ed0062913ae417eeccec243b430d444495efde2a8e9c4c02e78ea",
|
||||
"cancellationSourceSha256": "a07d1a9e982dec0ae130c1ad1efa5bc75c6a39e013b730ba3ee45256f09ceeca",
|
||||
"agentFilesSourceSha256": "fb32f6badc13dfa22f231479509b631b2f0eba0c99b1ab9f3cd5c0c601aa03ba",
|
||||
"nativeDistributionSourceSha256": "aed327c389e957483a503cb1bb8b247ee7cd7ff4b528fb4567c39bcb4577690b"
|
||||
}
|
||||
}
|
||||
@@ -62,7 +62,6 @@ async function fixture(provider: "anthropic" | "openai", method: "api_key" | "su
|
||||
const companyId = randomUUID();
|
||||
const agentId = randomUUID();
|
||||
const userId = `owner-${companyId}`;
|
||||
await db.insert(authUsers).values({ id: userId, name: "Manager owner", email: `${userId}@fixture.invalid`, emailVerified: true, createdAt: new Date(), updatedAt: new Date() });
|
||||
const binding = { provider, method, mode: "responsible_user" } as const;
|
||||
const adapterType = provider === "anthropic" ? "claude_local" : "codex_local";
|
||||
await db.insert(authUsers).values({ id: userId, name: "Connection owner", email: `${userId}@example.test`,
|
||||
|
||||
@@ -15,6 +15,10 @@ import { accessService } from "../services/access.js";
|
||||
import { instanceSettingsService } from "../services/instance-settings.js";
|
||||
import { PaperclipRunnerToolAuthority } from "../services/native-runtime/paperclip-runner-tool-authority.js";
|
||||
|
||||
vi.mock("../services/agent-lifecycle-driver.js", () => ({
|
||||
createLifecycleDriver: () => ({ requiredPluginIds: async () => [], runPlugin: async () => "complete", runHost: async () => "complete" }),
|
||||
}));
|
||||
|
||||
// All requests traverse the assembled server and production bearer middleware.
|
||||
// No req.actor injection or special permission grants establish access.
|
||||
describe("connection management with production default agents", () => {
|
||||
@@ -74,6 +78,7 @@ describe("connection management with production default agents", () => {
|
||||
const f = await fixture();
|
||||
const grants = await server.db.select().from(principalPermissionGrants).where(eq(principalPermissionGrants.principalId, f.agentId));
|
||||
expect(grants.some(g => g.permissionKey === "tools:admin")).toBe(false);
|
||||
expect(await f.request("GET", `/companies/${f.companyId}/tools/stdio-templates`)).toMatchObject({ status: 403 });
|
||||
const created = await f.request("POST", `/companies/${f.companyId}/tools/connections`, { name: "Fixture tool", transport: "mcp_remote", authKind: "none", transportConfig: { url: "https://1.1.1.1/mcp" } });
|
||||
expect(created, JSON.stringify(created)).toMatchObject({ status: 201 });
|
||||
expect(await f.request("PATCH", `/tool-connections/${created.body.id}`, { name: "Updated tool" }, f.jwt)).toMatchObject({ status: 200, body: { name: "Updated tool" } });
|
||||
|
||||
@@ -46,6 +46,8 @@ export function responsibleUserForAiRequest(req: Request): string | null {
|
||||
}
|
||||
|
||||
export async function canManageAiConnections(db: Db, req: Request, companyId: string): Promise<boolean> {
|
||||
const memberships = req.actor.type === "agent" ? req.actor.onBehalfOfMemberships : req.actor.memberships;
|
||||
if (memberships?.some(m => m.companyId === companyId && m.status === "active" && m.membershipRole === "viewer")) return false;
|
||||
return (await connectionManagementService(db).capability(req.actor, "connections:configure", {
|
||||
type: "connection_management", companyId, purpose: "ai", operation: "manage",
|
||||
}, { mutation: false })).allowed;
|
||||
|
||||
@@ -1505,6 +1505,7 @@ const BOARD_ONLY_OPERATIONS = new Set([
|
||||
"POST /api/agents/{id}/connection-intents/{interactionId}/adopt",
|
||||
"POST /api/connection-intents/{interactionId}/decline",
|
||||
"POST /api/companies/{companyId}/tools/action-requests/{actionRequestId}/trust-rule",
|
||||
"GET /api/companies/{companyId}/tools/stdio-templates",
|
||||
"POST /api/companies/{companyId}/tools/stdio-templates",
|
||||
"POST /api/companies/{companyId}/tools/stdio-templates/{templateId}/disable",
|
||||
"POST /api/companies/{companyId}/tools/mcp/import-json",
|
||||
|
||||
@@ -779,6 +779,7 @@ function connectorEnrollmentPrincipal(req: Request): string {
|
||||
}
|
||||
|
||||
async function assertToolsAdmin(req: Request, companyId: string) {
|
||||
assertBoard(req);
|
||||
await assertBoardToolPermission(req, companyId, "tools:admin");
|
||||
}
|
||||
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import type { AiConnectionList, AiConnectionUsage, CreateAiConnection, AiConnectionLoginIntent, LocalAiLoginAttempt, LocalAiLoginStatus } from "@paperclipai/shared";
|
||||
import { api } from "./client";
|
||||
export const aiConnectionsApi = {
|
||||
loginSetup: (companyId: string, sessionId: string) => api.get<{ sessionId: string; intent: AiConnectionLoginIntent; environmentId: string; status: string; connectionId: string | null; grantId: string | null }>(`/companies/${companyId}/ai-connections/login/${encodeURIComponent(sessionId)}/setup`),
|
||||
probeUsage: (companyId: string, connectionId: string, grantId?: string) => api.get<AiConnectionUsage>(`/companies/${companyId}/ai-connections/${connectionId}/usage${grantId ? `?grantId=${encodeURIComponent(grantId)}` : ""}`),
|
||||
startLocalLogin: (companyId: string, input: AiConnectionLoginIntent & { restart?: boolean }) => api.post<LocalAiLoginAttempt>(`/companies/${companyId}/ai-connections/local/attempts`, input),
|
||||
checkLocalLogin: (companyId: string, input: AiConnectionLoginIntent & { localSessionId?: string }) => api.post<LocalAiLoginStatus>(`/companies/${companyId}/ai-connections/local/check`, input),
|
||||
|
||||
@@ -237,20 +237,6 @@ afterEach(async () => {
|
||||
});
|
||||
|
||||
describe("ConnectionIntentInteractionBody states and audience", () => {
|
||||
it("opens the saved chat authentication step without another setup form", async () => {
|
||||
const interaction = { ...pendingConnectionIntentInteraction, payload: { ...pendingConnectionIntentInteraction.payload,
|
||||
purpose: "channel" as const, serviceSlug: "slack", serviceName: "Slack", phase: "authorizing" as const,
|
||||
savedSetup: { endpointId: "endpoint", connectionId: "connection" } } };
|
||||
setupOptionsMock.mockResolvedValue({ version: 1, interaction, requestedAgentId: interaction.payload.requestingAgentId,
|
||||
existingConnections: [], service: { service: "slack", name: "Slack", methods: [], state: "needs_user_action" },
|
||||
chatSetup: { endpointId: "endpoint", connectionId: "connection", path: "/ACME/apps/chat/connect?provider=slack&resume=endpoint" } });
|
||||
renderBody(interaction);
|
||||
await flush();
|
||||
expect(document.querySelector('a')?.getAttribute("href")).toBe("/ACME/apps/chat/connect?provider=slack&resume=endpoint");
|
||||
expect(document.querySelector('[data-testid="shared-connection-setup"]')).toBeNull();
|
||||
expect(completeMock).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it.each([
|
||||
[pendingConnectionIntentInteraction, "Connect"],
|
||||
[
|
||||
|
||||
@@ -64,7 +64,6 @@ export function ConnectionIntentInteractionBody({
|
||||
);
|
||||
const isPending = interaction.status === "pending";
|
||||
const isAi = interaction.payload.purpose === "ai";
|
||||
const isChatHandoff = Boolean(interaction.payload.savedSetup);
|
||||
const provider = isAi ? aiProviderSchema.safeParse(interaction.payload.serviceSlug).data : undefined;
|
||||
const serviceName = provider ? AI_CONNECTION_CAPABILITIES[provider].name : interaction.payload.serviceName;
|
||||
const isEmail = interaction.payload.purpose === "channel" && interaction.payload.serviceSlug === "agentmail";
|
||||
@@ -473,7 +472,6 @@ export function ConnectionIntentInteractionBody({
|
||||
? `${interaction.payload.requestingAgentName} needs your own AI connection. Connect here and the task will resume automatically.`
|
||||
: interaction.payload.purpose === "ai"
|
||||
? "This task can’t run until the agent has a valid AI connection. Connect here and the task will resume automatically."
|
||||
: isChatHandoff ? "Complete the human sign-in step in the saved setup. Your task continues when the connection is ready."
|
||||
: isEmail ? "Connect AgentMail to create an email address for this agent."
|
||||
: "Connect your identity or reuse an eligible connection. Access is added only for this agent."}
|
||||
</p>
|
||||
@@ -488,10 +486,7 @@ export function ConnectionIntentInteractionBody({
|
||||
</p>
|
||||
) : null}
|
||||
|
||||
{isChatHandoff ? <div className="mt-4 flex items-center justify-between gap-2">
|
||||
<Button type="button" variant="ghost" disabled={declineMutation.isPending} onClick={() => declineMutation.mutate()}>Not now</Button>
|
||||
{setupQuery.data?.chatSetup ? <Button asChild><a href={setupQuery.data.chatSetup.path} target="_blank" rel="noopener noreferrer">Continue authentication</a></Button> : setupContent}
|
||||
</div> : isEmail ? setupQuery.isLoading || setupQuery.isError ? <>
|
||||
{isEmail ? setupQuery.isLoading || setupQuery.isError ? <>
|
||||
{setupContent}
|
||||
<Button type="button" variant="ghost" disabled={declineMutation.isPending} onClick={() => declineMutation.mutate()}>Not now</Button>
|
||||
</> : <AgentMailIntentSetup
|
||||
|
||||
@@ -445,28 +445,6 @@ describe("AppsConnect — Connect with a link (M4 frame)", () => {
|
||||
expect(startOAuthMock).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("opens the saved AI sign-in and reuses a completed account", async () => {
|
||||
mockSearch.value = "aiLogin=saved-login";
|
||||
const setup = vi.spyOn(aiConnectionsApi, "loginSetup").mockResolvedValue({
|
||||
sessionId: "saved-login", environmentId: "local-env", status: "completed", connectionId: "saved-account", grantId: "saved-grant",
|
||||
intent: { provider: "openai", method: "subscription", name: "Saved account", ownership: "personal", agentIds: ["agent-1"], allAgents: false },
|
||||
});
|
||||
await render();
|
||||
expect(setup).toHaveBeenCalledWith("company-1", "saved-login");
|
||||
expect(container.textContent).toContain("This account is connected.");
|
||||
expect(connectAppMock).not.toHaveBeenCalled();
|
||||
await act(async () => buttonByText("View connection")?.click());
|
||||
expect(mockNavigate).toHaveBeenCalledWith("/apps/saved-account/permissions");
|
||||
});
|
||||
|
||||
it("shows an unavailable saved sign-in without creating a replacement", async () => {
|
||||
mockSearch.value = "aiLogin=another-users-login";
|
||||
vi.spyOn(aiConnectionsApi, "loginSetup").mockRejectedValue(new Error("Sign-in attempt not found"));
|
||||
await render();
|
||||
expect(container.querySelector('[role="alert"]')?.textContent).toBe("Sign-in attempt not found");
|
||||
expect(connectAppMock).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("keeps Honcho setup blocked until the provider workspace is supplied", async () => {
|
||||
const honcho = CONNECTABLE_APP_DEFINITIONS.find((app) => app.slug === "honcho")!;
|
||||
experimentalMock.mockResolvedValue({ enableMemoryConnectors: true });
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { aiConnectionsApi } from "@/api/ai-connections";
|
||||
import { AiConnectionCredentialStep } from "@/components/ai-connections/AiConnectionCredentialStep";
|
||||
import { AiProviderSetup } from "@/components/ai-connections/AiProviderSetup";
|
||||
import { AiConnectionPoolConnector } from "@/components/ai-connections/AiConnectionPoolConnector";
|
||||
import { useEffect } from "react";
|
||||
@@ -28,9 +27,6 @@ export function AppsConnect({ byoOnly = false, credentialSource = "paperclip_vau
|
||||
const { appKey } = useParams<{ appKey?: string }>();
|
||||
const source = searchParams.get("source") ?? appKey ?? searchParams.get("appKey");
|
||||
const reconnectId = searchParams.get("reconnect");
|
||||
const aiLoginId = searchParams.get("aiLogin");
|
||||
const login = useQuery({ queryKey: ["ai-login-setup", selectedCompanyId, aiLoginId],
|
||||
queryFn: () => aiConnectionsApi.loginSetup(selectedCompanyId!, aiLoginId!), enabled: Boolean(selectedCompanyId && aiLoginId), retry: false });
|
||||
const preset = aiProviderSetupPreset(source);
|
||||
const aiReconnectRequested = Boolean(reconnectId && (preset || ["openai", "anthropic", "xai"].includes(source ?? "")));
|
||||
const aiAccounts = useQuery({ queryKey: ["ai-connections", selectedCompanyId], queryFn: () => aiConnectionsApi.list(selectedCompanyId!), enabled: Boolean(selectedCompanyId && aiReconnectRequested) });
|
||||
@@ -59,16 +55,6 @@ export function AppsConnect({ byoOnly = false, credentialSource = "paperclip_vau
|
||||
const path = selectedCompanyId && consumeSkillSourceReturn(selectedCompanyId);
|
||||
if (path) navigate(path);
|
||||
}
|
||||
if (aiLoginId && selectedCompanyId) {
|
||||
if (login.isPending) return <p role="status">Loading saved sign-in…</p>;
|
||||
if (!login.data) return <p role="alert">{login.error?.message ?? "This sign-in is unavailable."}</p>;
|
||||
const { intent, environmentId, connectionId, grantId } = login.data;
|
||||
if (connectionId && grantId) return <div className="space-y-4"><p role="status">This account is connected.</p><Button onClick={() => navigate(`/apps/${connectionId}/permissions`)}>View connection</Button></div>;
|
||||
return <AiConnectionCredentialStep key={aiLoginId} companyId={selectedCompanyId} {...intent}
|
||||
agentIds={intent.agentIds ?? []} allAgents={intent.allAgents ?? false} initialMethod={intent.method} fixedMethod
|
||||
environmentId={environmentId} hideName onCancel={() => navigate("/apps")}
|
||||
onComplete={binding => navigate(`/apps/${binding.connectionId}/permissions`)} />;
|
||||
}
|
||||
if (aiReconnectRequested && aiAccounts.isPending) return <p role="status">Loading connection…</p>;
|
||||
if (aiReconnectRequested && aiAccounts.isError) return <p role="alert">Could not load this connection. Refresh to try again.</p>;
|
||||
if (aiReconnectRequested && !aiReconnect) return <p role="alert">This connection is unavailable. Return to Connectors to choose an account.</p>;
|
||||
|
||||
Reference in new issue
Block a user