@borgee/agents-host 0.2.2 → 0.2.28
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +184 -21
- package/dist/agents-host-supervisor.d.ts +7 -5
- package/dist/agents-host-supervisor.js +24 -4
- package/dist/agents-host.d.ts +114 -15
- package/dist/agents-host.js +2712 -141
- package/dist/chat/chat-control-plane.d.ts +13 -2
- package/dist/chat/sdk-chat-control-plane.d.ts +14 -3
- package/dist/chat/sdk-chat-control-plane.js +54 -2
- package/dist/cli-args.d.ts +46 -5
- package/dist/cli-args.js +313 -32
- package/dist/cli.d.ts +9 -0
- package/dist/cli.js +112 -5
- package/dist/compatibility-gates.d.ts +39 -0
- package/dist/compatibility-gates.js +131 -0
- package/dist/config.d.ts +1 -0
- package/dist/config.js +23 -5
- package/dist/connections-state-store.d.ts +81 -0
- package/dist/connections-state-store.js +228 -0
- package/dist/context/attention.d.ts +12 -0
- package/dist/context/attention.js +137 -0
- package/dist/context/collaboration-capabilities-diagnostics.d.ts +3 -0
- package/dist/context/collaboration-capabilities-diagnostics.js +18 -0
- package/dist/context/collaboration-outcome.d.ts +2 -0
- package/dist/context/collaboration-outcome.js +26 -0
- package/dist/context/injection.d.ts +134 -0
- package/dist/context/injection.js +355 -0
- package/dist/context/prompt.d.ts +4 -1
- package/dist/context/prompt.js +231 -1
- package/dist/context/task-thread-collaboration.d.ts +6 -0
- package/dist/context/task-thread-collaboration.js +31 -0
- package/dist/context/turn-preparation.d.ts +9 -0
- package/dist/context/turn-preparation.js +135 -0
- package/dist/debug.d.ts +44 -0
- package/dist/debug.js +135 -0
- package/dist/gateway/localhost-gateway.d.ts +52 -0
- package/dist/gateway/localhost-gateway.js +857 -0
- package/dist/index.js +7 -5
- package/dist/local-config.d.ts +4 -1
- package/dist/local-config.js +24 -7
- package/dist/managed-daemon-log.d.ts +34 -0
- package/dist/managed-daemon-log.js +261 -0
- package/dist/managed-daemon.d.ts +220 -0
- package/dist/managed-daemon.js +1601 -0
- package/dist/policy/authorization-audit.d.ts +63 -0
- package/dist/policy/authorization-audit.js +94 -0
- package/dist/policy/copilot-permission.d.ts +15 -0
- package/dist/policy/copilot-permission.js +193 -0
- package/dist/policy/gateway-authorization.d.ts +42 -0
- package/dist/policy/gateway-authorization.js +162 -0
- package/dist/providers/awaiting-user.d.ts +12 -0
- package/dist/providers/awaiting-user.js +192 -0
- package/dist/providers/claude/adapter.d.ts +3 -1
- package/dist/providers/claude/adapter.js +8 -12
- package/dist/providers/claude/cli-client.d.ts +12 -5
- package/dist/providers/claude/cli-client.js +184 -37
- package/dist/providers/claude/session-store.d.ts +1 -0
- package/dist/providers/codex/adapter.d.ts +11 -0
- package/dist/providers/codex/adapter.js +19 -0
- package/dist/providers/codex/cli-client.d.ts +103 -0
- package/dist/providers/codex/cli-client.js +1133 -0
- package/dist/providers/codex/project-doc.d.ts +3 -0
- package/dist/providers/codex/project-doc.js +90 -0
- package/dist/providers/codex/session-store.d.ts +38 -0
- package/dist/providers/codex/session-store.js +150 -0
- package/dist/providers/copilot/adapter.d.ts +3 -1
- package/dist/providers/copilot/adapter.js +8 -12
- package/dist/providers/copilot/cli-client.d.ts +20 -2
- package/dist/providers/copilot/cli-client.js +251 -71
- package/dist/providers/copilot/session-store.d.ts +1 -0
- package/dist/providers/create-provider.d.ts +11 -2
- package/dist/providers/create-provider.js +131 -12
- package/dist/run.d.ts +1 -0
- package/dist/run.js +5 -2
- package/dist/state-paths.d.ts +14 -1
- package/dist/state-paths.js +87 -3
- package/dist/task-thread-resolution.d.ts +10 -0
- package/dist/task-thread-resolution.js +48 -0
- package/dist/types.d.ts +267 -1
- package/dist/visible-mentions.d.ts +3 -0
- package/dist/visible-mentions.js +15 -0
- package/package.json +19 -17
- package/skills/borgee-agent/SKILL.md +33 -0
- package/skills/borgee-agent/borgee-agent.mjs +473 -0
- package/skills/borgee-agent/borgee-agent.py +409 -0
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import type { DebugLogger } from '../debug.js';
|
|
2
|
+
import type { InternalPolicyMode } from '../compatibility-gates.js';
|
|
3
|
+
export type AuthorizationAuditSurface = 'copilot-acp-permission' | 'localhost-gateway';
|
|
4
|
+
export interface AuthorizationAuditRecord {
|
|
5
|
+
timestamp: string;
|
|
6
|
+
surface: AuthorizationAuditSurface;
|
|
7
|
+
policyMode: InternalPolicyMode;
|
|
8
|
+
effectiveOutcome: string;
|
|
9
|
+
reason: string;
|
|
10
|
+
agentId?: string;
|
|
11
|
+
channelId?: string;
|
|
12
|
+
selectedOptionKind?: string;
|
|
13
|
+
policyOutcome?: string;
|
|
14
|
+
policySelectedOptionKind?: string;
|
|
15
|
+
toolKind?: string;
|
|
16
|
+
method?: string;
|
|
17
|
+
path?: string;
|
|
18
|
+
status?: number;
|
|
19
|
+
}
|
|
20
|
+
interface AuthorizationAuditFileSystem {
|
|
21
|
+
mkdir(path: string, options: {
|
|
22
|
+
recursive: boolean;
|
|
23
|
+
mode: number;
|
|
24
|
+
}): Promise<void>;
|
|
25
|
+
stat(path: string): Promise<{
|
|
26
|
+
size: number;
|
|
27
|
+
}>;
|
|
28
|
+
rm(path: string, options: {
|
|
29
|
+
force: boolean;
|
|
30
|
+
}): Promise<void>;
|
|
31
|
+
rename(from: string, to: string): Promise<void>;
|
|
32
|
+
appendFile(path: string, data: string, options: {
|
|
33
|
+
encoding: BufferEncoding;
|
|
34
|
+
mode: number;
|
|
35
|
+
}): Promise<void>;
|
|
36
|
+
chmod(path: string, mode: number): Promise<void>;
|
|
37
|
+
}
|
|
38
|
+
export interface AuthorizationAuditSinkOptions {
|
|
39
|
+
stateRootDir: string;
|
|
40
|
+
logger?: DebugLogger;
|
|
41
|
+
maxFileBytes?: number;
|
|
42
|
+
fileSystem?: AuthorizationAuditFileSystem;
|
|
43
|
+
}
|
|
44
|
+
export interface AuthorizationAuditSinkLike {
|
|
45
|
+
append(record: AuthorizationAuditRecord): void;
|
|
46
|
+
drain(): Promise<void>;
|
|
47
|
+
}
|
|
48
|
+
export declare class AuthorizationAuditSink implements AuthorizationAuditSinkLike {
|
|
49
|
+
private readonly logger;
|
|
50
|
+
private readonly fileSystem;
|
|
51
|
+
private readonly currentPath;
|
|
52
|
+
private readonly previousPath;
|
|
53
|
+
private readonly maxFileBytes;
|
|
54
|
+
private writeQueue;
|
|
55
|
+
private lastWriteError;
|
|
56
|
+
constructor(options: AuthorizationAuditSinkOptions);
|
|
57
|
+
append(record: AuthorizationAuditRecord): void;
|
|
58
|
+
drain(): Promise<void>;
|
|
59
|
+
private writePayload;
|
|
60
|
+
private rotateIfNeeded;
|
|
61
|
+
private readFileSize;
|
|
62
|
+
}
|
|
63
|
+
export {};
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import { dirname } from 'node:path';
|
|
2
|
+
import { promises as fs } from 'node:fs';
|
|
3
|
+
import { HostLogger, summarizeError } from '../debug.js';
|
|
4
|
+
import { resolveAuthorizationAuditPath, resolvePreviousAuthorizationAuditPath } from '../state-paths.js';
|
|
5
|
+
const PRIVATE_AUDIT_FILE_MODE = 0o600;
|
|
6
|
+
const PRIVATE_AUDIT_DIRECTORY_MODE = 0o700;
|
|
7
|
+
const DEFAULT_MAX_AUDIT_FILE_BYTES = 32 * 1024;
|
|
8
|
+
const DEFAULT_FILE_SYSTEM = {
|
|
9
|
+
async mkdir(path, options) {
|
|
10
|
+
await fs.mkdir(path, options);
|
|
11
|
+
},
|
|
12
|
+
async stat(path) {
|
|
13
|
+
return fs.stat(path);
|
|
14
|
+
},
|
|
15
|
+
async rm(path, options) {
|
|
16
|
+
await fs.rm(path, options);
|
|
17
|
+
},
|
|
18
|
+
async rename(from, to) {
|
|
19
|
+
await fs.rename(from, to);
|
|
20
|
+
},
|
|
21
|
+
async appendFile(path, data, options) {
|
|
22
|
+
await fs.appendFile(path, data, options);
|
|
23
|
+
},
|
|
24
|
+
async chmod(path, mode) {
|
|
25
|
+
await fs.chmod(path, mode);
|
|
26
|
+
},
|
|
27
|
+
};
|
|
28
|
+
export class AuthorizationAuditSink {
|
|
29
|
+
logger;
|
|
30
|
+
fileSystem;
|
|
31
|
+
currentPath;
|
|
32
|
+
previousPath;
|
|
33
|
+
maxFileBytes;
|
|
34
|
+
writeQueue = Promise.resolve();
|
|
35
|
+
lastWriteError = null;
|
|
36
|
+
constructor(options) {
|
|
37
|
+
this.logger = options.logger ?? new HostLogger();
|
|
38
|
+
this.fileSystem = options.fileSystem ?? DEFAULT_FILE_SYSTEM;
|
|
39
|
+
this.currentPath = resolveAuthorizationAuditPath(options.stateRootDir);
|
|
40
|
+
this.previousPath = resolvePreviousAuthorizationAuditPath(options.stateRootDir);
|
|
41
|
+
this.maxFileBytes = options.maxFileBytes ?? DEFAULT_MAX_AUDIT_FILE_BYTES;
|
|
42
|
+
}
|
|
43
|
+
append(record) {
|
|
44
|
+
const payload = `${JSON.stringify(record)}\n`;
|
|
45
|
+
const operation = this.writeQueue.then(() => this.writePayload(payload), () => this.writePayload(payload));
|
|
46
|
+
this.writeQueue = operation.catch((error) => {
|
|
47
|
+
this.lastWriteError = error instanceof Error ? error : new Error(String(error));
|
|
48
|
+
this.logger.error('failed to persist authorization audit record', {
|
|
49
|
+
auditPath: this.currentPath,
|
|
50
|
+
error: summarizeError(this.lastWriteError),
|
|
51
|
+
});
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
async drain() {
|
|
55
|
+
await this.writeQueue;
|
|
56
|
+
if (this.lastWriteError) {
|
|
57
|
+
throw this.lastWriteError;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
async writePayload(payload) {
|
|
61
|
+
await this.fileSystem.mkdir(dirname(this.currentPath), {
|
|
62
|
+
recursive: true,
|
|
63
|
+
mode: PRIVATE_AUDIT_DIRECTORY_MODE,
|
|
64
|
+
});
|
|
65
|
+
await this.rotateIfNeeded(Buffer.byteLength(payload, 'utf8'));
|
|
66
|
+
await this.fileSystem.appendFile(this.currentPath, payload, {
|
|
67
|
+
encoding: 'utf8',
|
|
68
|
+
mode: PRIVATE_AUDIT_FILE_MODE,
|
|
69
|
+
});
|
|
70
|
+
await this.fileSystem.chmod(this.currentPath, PRIVATE_AUDIT_FILE_MODE);
|
|
71
|
+
}
|
|
72
|
+
async rotateIfNeeded(nextPayloadBytes) {
|
|
73
|
+
const currentSize = await this.readFileSize(this.currentPath);
|
|
74
|
+
if (currentSize === 0 || currentSize + nextPayloadBytes <= this.maxFileBytes) {
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
await this.fileSystem.rm(this.previousPath, { force: true }).catch(() => { });
|
|
78
|
+
await this.fileSystem.rename(this.currentPath, this.previousPath);
|
|
79
|
+
await this.fileSystem.chmod(this.previousPath, PRIVATE_AUDIT_FILE_MODE).catch(() => { });
|
|
80
|
+
}
|
|
81
|
+
async readFileSize(path) {
|
|
82
|
+
try {
|
|
83
|
+
const stats = await this.fileSystem.stat(path);
|
|
84
|
+
return stats.size;
|
|
85
|
+
}
|
|
86
|
+
catch (error) {
|
|
87
|
+
const code = error instanceof Error && 'code' in error ? error.code : undefined;
|
|
88
|
+
if (code === 'ENOENT') {
|
|
89
|
+
return 0;
|
|
90
|
+
}
|
|
91
|
+
throw error;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { RequestPermissionRequest, RequestPermissionResponse } from '@agentclientprotocol/sdk';
|
|
2
|
+
import type { DebugLogger } from '../debug.js';
|
|
3
|
+
import type { InternalPolicyMode } from '../compatibility-gates.js';
|
|
4
|
+
import type { AuthorizationAuditSinkLike } from './authorization-audit.js';
|
|
5
|
+
export interface CopilotPermissionPolicyContext {
|
|
6
|
+
gateEnabled: boolean;
|
|
7
|
+
policyMode: InternalPolicyMode;
|
|
8
|
+
params: RequestPermissionRequest;
|
|
9
|
+
logger: DebugLogger;
|
|
10
|
+
auditSink?: AuthorizationAuditSinkLike;
|
|
11
|
+
agentId?: string;
|
|
12
|
+
channelId?: string;
|
|
13
|
+
}
|
|
14
|
+
export declare function resolveCopilotPermissionResponse(context: CopilotPermissionPolicyContext): RequestPermissionResponse;
|
|
15
|
+
export declare function createLegacyMissingAllowOptionError(): Error;
|
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
import { summarizeError } from '../debug.js';
|
|
2
|
+
const LEGACY_MISSING_ALLOW_OPTION_MESSAGE = 'Copilot ACP requested permission but did not offer an allow option';
|
|
3
|
+
function legacySelectPermissionOption(options) {
|
|
4
|
+
const option = options.find((candidate) => candidate.kind === 'allow_always')
|
|
5
|
+
?? options.find((candidate) => candidate.kind === 'allow_once');
|
|
6
|
+
if (!option) {
|
|
7
|
+
throw new Error(LEGACY_MISSING_ALLOW_OPTION_MESSAGE);
|
|
8
|
+
}
|
|
9
|
+
return option;
|
|
10
|
+
}
|
|
11
|
+
function resolveRejectOption(options) {
|
|
12
|
+
return options.find((candidate) => candidate.kind === 'reject_once')
|
|
13
|
+
?? options.find((candidate) => candidate.kind === 'reject_always');
|
|
14
|
+
}
|
|
15
|
+
function normalizeToolKind(value) {
|
|
16
|
+
switch (value) {
|
|
17
|
+
case 'read':
|
|
18
|
+
case 'edit':
|
|
19
|
+
case 'delete':
|
|
20
|
+
case 'move':
|
|
21
|
+
case 'search':
|
|
22
|
+
case 'execute':
|
|
23
|
+
case 'think':
|
|
24
|
+
case 'fetch':
|
|
25
|
+
case 'switch_mode':
|
|
26
|
+
case 'other':
|
|
27
|
+
return value;
|
|
28
|
+
default:
|
|
29
|
+
return 'unknown';
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
function selectPreferredAllowOption(options, toolKind) {
|
|
33
|
+
const prefersAlways = toolKind === 'read' || toolKind === 'search';
|
|
34
|
+
if (prefersAlways) {
|
|
35
|
+
return options.find((candidate) => candidate.kind === 'allow_always')
|
|
36
|
+
?? options.find((candidate) => candidate.kind === 'allow_once');
|
|
37
|
+
}
|
|
38
|
+
return options.find((candidate) => candidate.kind === 'allow_once')
|
|
39
|
+
?? options.find((candidate) => candidate.kind === 'allow_always');
|
|
40
|
+
}
|
|
41
|
+
function evaluatePermissionDecision(params) {
|
|
42
|
+
const toolKind = normalizeToolKind(params.toolCall.kind);
|
|
43
|
+
const allowOption = selectPreferredAllowOption(params.options, toolKind);
|
|
44
|
+
if (!allowOption) {
|
|
45
|
+
return null;
|
|
46
|
+
}
|
|
47
|
+
return {
|
|
48
|
+
outcome: 'selected',
|
|
49
|
+
optionId: allowOption.optionId,
|
|
50
|
+
optionKind: allowOption.kind,
|
|
51
|
+
reason: toolKind === 'read' || toolKind === 'search'
|
|
52
|
+
? 'readonly-tool-preferred'
|
|
53
|
+
: 'mutating-or-unknown-tool-preferred',
|
|
54
|
+
toolKind,
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
function decisionToResponse(decision) {
|
|
58
|
+
return decision.outcome === 'selected'
|
|
59
|
+
? {
|
|
60
|
+
outcome: {
|
|
61
|
+
outcome: 'selected',
|
|
62
|
+
optionId: decision.optionId,
|
|
63
|
+
},
|
|
64
|
+
}
|
|
65
|
+
: {
|
|
66
|
+
outcome: {
|
|
67
|
+
outcome: 'cancelled',
|
|
68
|
+
},
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
function appendAuditRecord(context, effectiveDecision, reason, policyDecision) {
|
|
72
|
+
context.auditSink?.append({
|
|
73
|
+
timestamp: new Date().toISOString(),
|
|
74
|
+
surface: 'copilot-acp-permission',
|
|
75
|
+
policyMode: context.policyMode,
|
|
76
|
+
effectiveOutcome: effectiveDecision?.outcome ?? 'error',
|
|
77
|
+
reason,
|
|
78
|
+
agentId: context.agentId,
|
|
79
|
+
channelId: context.channelId,
|
|
80
|
+
selectedOptionKind: effectiveDecision?.outcome === 'selected' ? effectiveDecision.optionKind : undefined,
|
|
81
|
+
policyOutcome: policyDecision?.outcome,
|
|
82
|
+
policySelectedOptionKind: policyDecision?.outcome === 'selected' ? policyDecision.optionKind : undefined,
|
|
83
|
+
toolKind: effectiveDecision?.toolKind ?? policyDecision?.toolKind ?? normalizeToolKind(context.params.toolCall.kind),
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
export function resolveCopilotPermissionResponse(context) {
|
|
87
|
+
if (!context.gateEnabled) {
|
|
88
|
+
const legacyOption = legacySelectPermissionOption(context.params.options);
|
|
89
|
+
return {
|
|
90
|
+
outcome: {
|
|
91
|
+
outcome: 'selected',
|
|
92
|
+
optionId: legacyOption.optionId,
|
|
93
|
+
},
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
try {
|
|
97
|
+
const decision = evaluatePermissionDecision(context.params);
|
|
98
|
+
if (decision) {
|
|
99
|
+
if (context.policyMode === 'audit-only') {
|
|
100
|
+
const legacyOption = legacySelectPermissionOption(context.params.options);
|
|
101
|
+
const effectiveDecision = {
|
|
102
|
+
outcome: 'selected',
|
|
103
|
+
optionId: legacyOption.optionId,
|
|
104
|
+
optionKind: legacyOption.kind,
|
|
105
|
+
reason: decision.reason,
|
|
106
|
+
toolKind: decision.toolKind,
|
|
107
|
+
};
|
|
108
|
+
appendAuditRecord(context, effectiveDecision, decision.reason, decision);
|
|
109
|
+
return decisionToResponse(effectiveDecision);
|
|
110
|
+
}
|
|
111
|
+
appendAuditRecord(context, decision, decision.reason, decision);
|
|
112
|
+
return decisionToResponse(decision);
|
|
113
|
+
}
|
|
114
|
+
const rejectOption = resolveRejectOption(context.params.options);
|
|
115
|
+
if (context.policyMode === 'audit-only') {
|
|
116
|
+
const hypotheticalDecision = rejectOption
|
|
117
|
+
? {
|
|
118
|
+
outcome: 'selected',
|
|
119
|
+
optionId: rejectOption.optionId,
|
|
120
|
+
optionKind: rejectOption.kind,
|
|
121
|
+
reason: 'no-allow-option',
|
|
122
|
+
toolKind: normalizeToolKind(context.params.toolCall.kind),
|
|
123
|
+
}
|
|
124
|
+
: {
|
|
125
|
+
outcome: 'cancelled',
|
|
126
|
+
reason: 'no-allow-option',
|
|
127
|
+
toolKind: normalizeToolKind(context.params.toolCall.kind),
|
|
128
|
+
};
|
|
129
|
+
appendAuditRecord(context, null, 'no-allow-option', hypotheticalDecision);
|
|
130
|
+
throw new Error(LEGACY_MISSING_ALLOW_OPTION_MESSAGE);
|
|
131
|
+
}
|
|
132
|
+
const rejectedDecision = rejectOption
|
|
133
|
+
? {
|
|
134
|
+
outcome: 'selected',
|
|
135
|
+
optionId: rejectOption.optionId,
|
|
136
|
+
optionKind: rejectOption.kind,
|
|
137
|
+
reason: 'no-allow-option',
|
|
138
|
+
toolKind: normalizeToolKind(context.params.toolCall.kind),
|
|
139
|
+
}
|
|
140
|
+
: {
|
|
141
|
+
outcome: 'cancelled',
|
|
142
|
+
reason: 'no-allow-option',
|
|
143
|
+
toolKind: normalizeToolKind(context.params.toolCall.kind),
|
|
144
|
+
};
|
|
145
|
+
appendAuditRecord(context, rejectedDecision, rejectedDecision.reason, rejectedDecision);
|
|
146
|
+
return decisionToResponse(rejectedDecision);
|
|
147
|
+
}
|
|
148
|
+
catch (error) {
|
|
149
|
+
if (error instanceof Error && error.message === LEGACY_MISSING_ALLOW_OPTION_MESSAGE) {
|
|
150
|
+
throw error;
|
|
151
|
+
}
|
|
152
|
+
const errorSummary = {
|
|
153
|
+
agentId: context.agentId,
|
|
154
|
+
channelId: context.channelId,
|
|
155
|
+
sessionId: context.params.sessionId,
|
|
156
|
+
toolKind: normalizeToolKind(context.params.toolCall.kind),
|
|
157
|
+
error: summarizeError(error),
|
|
158
|
+
};
|
|
159
|
+
if (context.policyMode === 'audit-only') {
|
|
160
|
+
context.logger.error('Copilot ACP policy evaluation failed; falling back to the legacy picker', errorSummary);
|
|
161
|
+
const legacyOption = legacySelectPermissionOption(context.params.options);
|
|
162
|
+
const fallbackDecision = {
|
|
163
|
+
outcome: 'selected',
|
|
164
|
+
optionId: legacyOption.optionId,
|
|
165
|
+
optionKind: legacyOption.kind,
|
|
166
|
+
reason: 'policy-evaluator-error-fallback',
|
|
167
|
+
toolKind: normalizeToolKind(context.params.toolCall.kind),
|
|
168
|
+
};
|
|
169
|
+
appendAuditRecord(context, fallbackDecision, fallbackDecision.reason);
|
|
170
|
+
return decisionToResponse(fallbackDecision);
|
|
171
|
+
}
|
|
172
|
+
context.logger.error('Copilot ACP policy evaluation failed; failing closed', errorSummary);
|
|
173
|
+
const rejectOption = resolveRejectOption(context.params.options);
|
|
174
|
+
const failedClosedDecision = rejectOption
|
|
175
|
+
? {
|
|
176
|
+
outcome: 'selected',
|
|
177
|
+
optionId: rejectOption.optionId,
|
|
178
|
+
optionKind: rejectOption.kind,
|
|
179
|
+
reason: 'policy-evaluator-error',
|
|
180
|
+
toolKind: normalizeToolKind(context.params.toolCall.kind),
|
|
181
|
+
}
|
|
182
|
+
: {
|
|
183
|
+
outcome: 'cancelled',
|
|
184
|
+
reason: 'policy-evaluator-error',
|
|
185
|
+
toolKind: normalizeToolKind(context.params.toolCall.kind),
|
|
186
|
+
};
|
|
187
|
+
appendAuditRecord(context, failedClosedDecision, failedClosedDecision.reason, failedClosedDecision);
|
|
188
|
+
return decisionToResponse(failedClosedDecision);
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
export function createLegacyMissingAllowOptionError() {
|
|
192
|
+
return new Error(LEGACY_MISSING_ALLOW_OPTION_MESSAGE);
|
|
193
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import type { IncomingMessage } from 'node:http';
|
|
2
|
+
export type GatewayDecisionReason = 'browser-origin-not-allowed' | 'method-not-allowed' | 'missing-or-invalid-token' | 'invalid-token' | 'channel-mismatch' | 'not-found' | 'bootstrap-unavailable' | 'authorized';
|
|
3
|
+
export interface GatewayChannelRoute {
|
|
4
|
+
channelId: string;
|
|
5
|
+
resource: 'bootstrap' | 'me' | 'history' | 'draft' | 'users' | 'messages' | 'tasks' | 'current-task';
|
|
6
|
+
}
|
|
7
|
+
export interface GatewayTaskRoute {
|
|
8
|
+
taskId: string;
|
|
9
|
+
resource: 'task';
|
|
10
|
+
}
|
|
11
|
+
export interface GatewayAuthorizationBinding {
|
|
12
|
+
channelId: string;
|
|
13
|
+
agentId?: string;
|
|
14
|
+
payloadAvailable: boolean;
|
|
15
|
+
}
|
|
16
|
+
export interface GatewayAuthorizationDecision {
|
|
17
|
+
reason: GatewayDecisionReason;
|
|
18
|
+
statusCode: number;
|
|
19
|
+
responseBody?: {
|
|
20
|
+
error: string;
|
|
21
|
+
} | {
|
|
22
|
+
status: 'ok';
|
|
23
|
+
};
|
|
24
|
+
allowHeader?: string;
|
|
25
|
+
binding?: GatewayAuthorizationBinding;
|
|
26
|
+
route?: GatewayChannelRoute | GatewayTaskRoute | {
|
|
27
|
+
resource: 'health';
|
|
28
|
+
};
|
|
29
|
+
path: string;
|
|
30
|
+
token?: string;
|
|
31
|
+
}
|
|
32
|
+
export interface GatewayAuthorizationInput {
|
|
33
|
+
request: IncomingMessage;
|
|
34
|
+
baseUrl: string;
|
|
35
|
+
allowCollaborationRoutes?: boolean;
|
|
36
|
+
lookup(token: string): GatewayAuthorizationBinding | null;
|
|
37
|
+
}
|
|
38
|
+
export declare function hasVisibleHeader(value: string | string[] | undefined): boolean;
|
|
39
|
+
export declare function parseBearerToken(value: string | string[] | undefined): string | null;
|
|
40
|
+
export declare function matchChannelRoute(pathname: string, allowCollaborationRoutes?: boolean): GatewayChannelRoute | null;
|
|
41
|
+
export declare function matchTaskRoute(pathname: string): GatewayTaskRoute | null;
|
|
42
|
+
export declare function evaluateGatewayAuthorization(input: GatewayAuthorizationInput): GatewayAuthorizationDecision;
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
export function hasVisibleHeader(value) {
|
|
2
|
+
if (Array.isArray(value)) {
|
|
3
|
+
return value.some((entry) => entry.trim().length > 0);
|
|
4
|
+
}
|
|
5
|
+
return typeof value === 'string' && value.trim().length > 0;
|
|
6
|
+
}
|
|
7
|
+
export function parseBearerToken(value) {
|
|
8
|
+
if (Array.isArray(value) || typeof value !== 'string') {
|
|
9
|
+
return null;
|
|
10
|
+
}
|
|
11
|
+
const trimmed = value.trim();
|
|
12
|
+
const prefix = 'Bearer ';
|
|
13
|
+
if (!trimmed.startsWith(prefix)) {
|
|
14
|
+
return null;
|
|
15
|
+
}
|
|
16
|
+
const token = trimmed.slice(prefix.length);
|
|
17
|
+
return token.length > 0 && !/\s/.test(token) ? token : null;
|
|
18
|
+
}
|
|
19
|
+
export function matchChannelRoute(pathname, allowCollaborationRoutes = false) {
|
|
20
|
+
const resources = allowCollaborationRoutes
|
|
21
|
+
? '(bootstrap|me|history|draft|users|messages|tasks|current-task)'
|
|
22
|
+
: '(bootstrap|me|history|tasks|current-task)';
|
|
23
|
+
const match = new RegExp(`^/v1/channels/([^/]+)/${resources}$`).exec(pathname);
|
|
24
|
+
if (!match) {
|
|
25
|
+
return null;
|
|
26
|
+
}
|
|
27
|
+
return {
|
|
28
|
+
channelId: decodeURIComponent(match[1] ?? ''),
|
|
29
|
+
resource: match[2],
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
export function matchTaskRoute(pathname) {
|
|
33
|
+
const match = /^\/v1\/tasks\/([^/]+)$/.exec(pathname);
|
|
34
|
+
if (!match) {
|
|
35
|
+
return null;
|
|
36
|
+
}
|
|
37
|
+
return {
|
|
38
|
+
taskId: decodeURIComponent(match[1] ?? ''),
|
|
39
|
+
resource: 'task',
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
function allowedMethodsForRoute(route, collaborationRoutesEnabled) {
|
|
43
|
+
switch (route.resource) {
|
|
44
|
+
case 'health':
|
|
45
|
+
case 'bootstrap':
|
|
46
|
+
case 'me':
|
|
47
|
+
case 'history':
|
|
48
|
+
case 'draft':
|
|
49
|
+
case 'users':
|
|
50
|
+
return ['GET'];
|
|
51
|
+
case 'messages':
|
|
52
|
+
return collaborationRoutesEnabled ? ['POST'] : [];
|
|
53
|
+
case 'tasks':
|
|
54
|
+
return ['GET', 'POST'];
|
|
55
|
+
case 'current-task':
|
|
56
|
+
case 'task':
|
|
57
|
+
return ['GET', 'PATCH'];
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
export function evaluateGatewayAuthorization(input) {
|
|
61
|
+
const url = new URL(input.request.url ?? '/', input.baseUrl);
|
|
62
|
+
const hiddenCollaborationRoute = !input.allowCollaborationRoutes
|
|
63
|
+
&& ['draft', 'users', 'messages'].includes(matchChannelRoute(url.pathname, true)?.resource ?? '');
|
|
64
|
+
if (hasVisibleHeader(input.request.headers.origin)) {
|
|
65
|
+
return {
|
|
66
|
+
reason: 'browser-origin-not-allowed',
|
|
67
|
+
statusCode: 403,
|
|
68
|
+
responseBody: { error: 'browser_origin_not_allowed' },
|
|
69
|
+
path: url.pathname,
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
const method = input.request.method ?? 'GET';
|
|
73
|
+
const collaborationRoutesEnabled = input.allowCollaborationRoutes ?? false;
|
|
74
|
+
const route = url.pathname === '/health'
|
|
75
|
+
? { resource: 'health' }
|
|
76
|
+
: matchChannelRoute(url.pathname, collaborationRoutesEnabled) ?? matchTaskRoute(url.pathname);
|
|
77
|
+
if (hiddenCollaborationRoute) {
|
|
78
|
+
return {
|
|
79
|
+
reason: 'not-found',
|
|
80
|
+
statusCode: 404,
|
|
81
|
+
responseBody: { error: 'not_found' },
|
|
82
|
+
path: url.pathname,
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
if (!route) {
|
|
86
|
+
return {
|
|
87
|
+
reason: 'not-found',
|
|
88
|
+
statusCode: 404,
|
|
89
|
+
responseBody: { error: 'not_found' },
|
|
90
|
+
path: url.pathname,
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
const allowedMethods = route ? allowedMethodsForRoute(route, collaborationRoutesEnabled) : [];
|
|
94
|
+
const methodAllowed = allowedMethods.includes(method);
|
|
95
|
+
if (!methodAllowed) {
|
|
96
|
+
return {
|
|
97
|
+
reason: 'method-not-allowed',
|
|
98
|
+
statusCode: 405,
|
|
99
|
+
responseBody: { error: 'method_not_allowed' },
|
|
100
|
+
allowHeader: allowedMethods.join(', '),
|
|
101
|
+
path: url.pathname,
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
if (route?.resource === 'health') {
|
|
105
|
+
return {
|
|
106
|
+
reason: 'authorized',
|
|
107
|
+
statusCode: 200,
|
|
108
|
+
responseBody: { status: 'ok' },
|
|
109
|
+
route,
|
|
110
|
+
path: url.pathname,
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
const token = parseBearerToken(input.request.headers.authorization);
|
|
114
|
+
if (!token) {
|
|
115
|
+
return {
|
|
116
|
+
reason: 'missing-or-invalid-token',
|
|
117
|
+
statusCode: 401,
|
|
118
|
+
responseBody: { error: 'missing_or_invalid_token' },
|
|
119
|
+
path: url.pathname,
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
const binding = input.lookup(token);
|
|
123
|
+
if (!binding) {
|
|
124
|
+
return {
|
|
125
|
+
reason: 'invalid-token',
|
|
126
|
+
statusCode: 401,
|
|
127
|
+
responseBody: { error: 'invalid_token' },
|
|
128
|
+
path: url.pathname,
|
|
129
|
+
token,
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
if ('channelId' in route && binding.channelId !== route.channelId) {
|
|
133
|
+
return {
|
|
134
|
+
reason: 'channel-mismatch',
|
|
135
|
+
statusCode: 403,
|
|
136
|
+
responseBody: { error: 'channel_mismatch' },
|
|
137
|
+
binding,
|
|
138
|
+
route,
|
|
139
|
+
path: url.pathname,
|
|
140
|
+
token,
|
|
141
|
+
};
|
|
142
|
+
}
|
|
143
|
+
if (route.resource === 'bootstrap' && !binding.payloadAvailable) {
|
|
144
|
+
return {
|
|
145
|
+
reason: 'bootstrap-unavailable',
|
|
146
|
+
statusCode: 404,
|
|
147
|
+
responseBody: { error: 'bootstrap_unavailable' },
|
|
148
|
+
binding,
|
|
149
|
+
route,
|
|
150
|
+
path: url.pathname,
|
|
151
|
+
token,
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
return {
|
|
155
|
+
reason: 'authorized',
|
|
156
|
+
statusCode: 200,
|
|
157
|
+
binding,
|
|
158
|
+
route,
|
|
159
|
+
path: url.pathname,
|
|
160
|
+
token,
|
|
161
|
+
};
|
|
162
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { ProviderGenerateOptions, ProviderReply } from '../types.js';
|
|
2
|
+
export declare const HOST_CONTROL_PREFIX = "[[BORGEE_CONTROL]] ";
|
|
3
|
+
export declare const AWAITING_USER_CONTROL_PREFIX = "[[BORGEE_CONTROL]] ";
|
|
4
|
+
export declare function parseProviderReply(text: string): ProviderReply;
|
|
5
|
+
export declare function sanitizeAwaitingUserProgressText(text: string): string;
|
|
6
|
+
export declare class AwaitingUserProgressSanitizer {
|
|
7
|
+
private readonly onProgress?;
|
|
8
|
+
private lastPublished;
|
|
9
|
+
constructor(onProgress?: ProviderGenerateOptions['onProgress']);
|
|
10
|
+
publish(text: string): void;
|
|
11
|
+
}
|
|
12
|
+
export declare function createAwaitingUserProgressHandler(onProgress?: ProviderGenerateOptions['onProgress']): ProviderGenerateOptions['onProgress'] | undefined;
|