chore(node-sdk): audit plugins shared
Add plugins/common.ts for the helpers and size limits copied across plugin files; managed-transaction compares snapshots in one place; guard template imported directly. Claude-Session: https://claude.ai/code/session_01Agoc9bRVE9KL7mmWtey9Vq
This commit is contained in:
1 parent
07f32a48d7
commit
b7f14ba34b
9 files changed
+55
-87
No files matched your search
+2
-1
@@ -13,7 +13,8 @@ import {
|
|||||||
import { writePrivateFile } from './secure-files.js';
|
import { writePrivateFile } from './secure-files.js';
|
||||||
import { SUPPORTED_AGENTS } from './plugins/registry.js';
|
import { SUPPORTED_AGENTS } from './plugins/registry.js';
|
||||||
import type { AgentPlugin, InstallConfig } from './plugins/base.js';
|
import type { AgentPlugin, InstallConfig } from './plugins/base.js';
|
||||||
import { generateHookScript, generateGuardScript } from './plugins/hook-template.js';
|
import { generateGuardScript } from './plugins/guard-template.js';
|
||||||
|
import { generateHookScript } from './plugins/hook-template.js';
|
||||||
import { augmentPlugin } from './plugins/augment.js';
|
import { augmentPlugin } from './plugins/augment.js';
|
||||||
import { claudecodePlugin } from './plugins/claudecode.js';
|
import { claudecodePlugin } from './plugins/claudecode.js';
|
||||||
import { cursorPlugin } from './plugins/cursor.js';
|
import { cursorPlugin } from './plugins/cursor.js';
|
||||||
|
|||||||
@@ -0,0 +1,31 @@
|
|||||||
|
import path from 'node:path';
|
||||||
|
|
||||||
|
export const MAX_SECRET_BYTES = 64 * 1024;
|
||||||
|
export const MAX_CONFIG_BYTES = 512 * 1024;
|
||||||
|
export const MAX_SOURCE_BYTES = 2 * 1024 * 1024;
|
||||||
|
|
||||||
|
export function asError(error: unknown): Error {
|
||||||
|
return error instanceof Error ? error : new Error(String(error));
|
||||||
|
}
|
||||||
|
|
||||||
|
export function errorMessage(error: unknown): string {
|
||||||
|
return error instanceof Error ? error.message : String(error);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function hasCode(error: unknown, code: string): boolean {
|
||||||
|
return typeof error === 'object' && error !== null && 'code' in error && error.code === code;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function samePath(left: string, right: string): boolean {
|
||||||
|
const normalizedLeft = path.resolve(left);
|
||||||
|
const normalizedRight = path.resolve(right);
|
||||||
|
return process.platform === 'win32'
|
||||||
|
? normalizedLeft.toLowerCase() === normalizedRight.toLowerCase()
|
||||||
|
: normalizedLeft === normalizedRight;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function sameAgentId(left: unknown, right: string): boolean {
|
||||||
|
return typeof left === 'string' && (process.platform === 'win32'
|
||||||
|
? left.toLowerCase() === right.toLowerCase()
|
||||||
|
: left === right);
|
||||||
|
}
|
||||||
@@ -1,8 +1,5 @@
|
|||||||
import { PROTECTED_RUNTIME_READER } from './runtime-reader-template.js';
|
import { PROTECTED_RUNTIME_READER } from './runtime-reader-template.js';
|
||||||
|
|
||||||
export { generateGuardScript } from './guard-template.js';
|
|
||||||
export type { GuardScriptOptions } from './guard-template.js';
|
|
||||||
|
|
||||||
export interface HookScriptOptions {
|
export interface HookScriptOptions {
|
||||||
readonly failClosed?: boolean;
|
readonly failClosed?: boolean;
|
||||||
readonly nativePayload?: boolean;
|
readonly nativePayload?: boolean;
|
||||||
|
|||||||
@@ -1,8 +1,7 @@
|
|||||||
import { constants } from 'node:fs';
|
import { constants } from 'node:fs';
|
||||||
import fsp from 'node:fs/promises';
|
import fsp from 'node:fs/promises';
|
||||||
import type { FileHandle } from 'node:fs/promises';
|
import type { FileHandle } from 'node:fs/promises';
|
||||||
|
import { asError, errorMessage, hasCode, MAX_SOURCE_BYTES } from './common.js';
|
||||||
const MAX_SOURCE_BYTES = 2 * 1024 * 1024;
|
|
||||||
|
|
||||||
export interface FileSnapshot {
|
export interface FileSnapshot {
|
||||||
readonly contents: string;
|
readonly contents: string;
|
||||||
@@ -11,18 +10,6 @@ export interface FileSnapshot {
|
|||||||
readonly mode: number;
|
readonly mode: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
function asError(error: unknown): Error {
|
|
||||||
return error instanceof Error ? error : new Error(String(error));
|
|
||||||
}
|
|
||||||
|
|
||||||
function errorMessage(error: unknown): string {
|
|
||||||
return error instanceof Error ? error.message : String(error);
|
|
||||||
}
|
|
||||||
|
|
||||||
function hasCode(error: unknown, code: string): boolean {
|
|
||||||
return typeof error === 'object' && error !== null && 'code' in error && error.code === code;
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function readPhysicalFile(
|
export async function readPhysicalFile(
|
||||||
filePath: string,
|
filePath: string,
|
||||||
label: string,
|
label: string,
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import os from 'node:os';
|
|||||||
import path from 'node:path';
|
import path from 'node:path';
|
||||||
import { ensurePrivateDirectory, resolvePrivateChildDirectory } from '../runtime-paths.js';
|
import { ensurePrivateDirectory, resolvePrivateChildDirectory } from '../runtime-paths.js';
|
||||||
import type { InstallConfig } from './base.js';
|
import type { InstallConfig } from './base.js';
|
||||||
|
import { MAX_CONFIG_BYTES, MAX_SECRET_BYTES, sameAgentId, samePath } from './common.js';
|
||||||
import { generateGuardScript, type GuardScriptOptions } from './guard-template.js';
|
import { generateGuardScript, type GuardScriptOptions } from './guard-template.js';
|
||||||
import { generateHookScript, type HookScriptOptions } from './hook-template.js';
|
import { generateHookScript, type HookScriptOptions } from './hook-template.js';
|
||||||
import {
|
import {
|
||||||
@@ -19,9 +20,6 @@ import {
|
|||||||
} from './managed-transaction.js';
|
} from './managed-transaction.js';
|
||||||
import { parseStrictJsonObject } from './strict-json.js';
|
import { parseStrictJsonObject } from './strict-json.js';
|
||||||
|
|
||||||
const MAX_SECRET_BYTES = 64 * 1024;
|
|
||||||
const MAX_CONFIG_BYTES = 512 * 1024;
|
|
||||||
|
|
||||||
export interface ManagedRuntimePaths {
|
export interface ManagedRuntimePaths {
|
||||||
readonly runtimeRoot: string;
|
readonly runtimeRoot: string;
|
||||||
readonly agentDirectory: string;
|
readonly agentDirectory: string;
|
||||||
@@ -79,14 +77,6 @@ interface RuntimeArtifactLocation {
|
|||||||
|
|
||||||
export type { RenameFile };
|
export type { RenameFile };
|
||||||
|
|
||||||
export function samePath(left: string, right: string): boolean {
|
|
||||||
const normalizedLeft = path.resolve(left);
|
|
||||||
const normalizedRight = path.resolve(right);
|
|
||||||
return process.platform === 'win32'
|
|
||||||
? normalizedLeft.toLowerCase() === normalizedRight.toLowerCase()
|
|
||||||
: normalizedLeft === normalizedRight;
|
|
||||||
}
|
|
||||||
|
|
||||||
function validateInstallConfig(config: InstallConfig, agentKey: string): void {
|
function validateInstallConfig(config: InstallConfig, agentKey: string): void {
|
||||||
for (const [field, value] of [
|
for (const [field, value] of [
|
||||||
['agentName', config.agentName],
|
['agentName', config.agentName],
|
||||||
@@ -146,12 +136,6 @@ export function managedRuntimePaths(
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
function sameAgentId(left: unknown, right: string): boolean {
|
|
||||||
return typeof left === 'string' && (process.platform === 'win32'
|
|
||||||
? left.toLowerCase() === right.toLowerCase()
|
|
||||||
: left === right);
|
|
||||||
}
|
|
||||||
|
|
||||||
async function validateRuntimeIdentity(
|
async function validateRuntimeIdentity(
|
||||||
paths: ManagedRuntimePaths,
|
paths: ManagedRuntimePaths,
|
||||||
agentKey: string,
|
agentKey: string,
|
||||||
|
|||||||
@@ -2,12 +2,10 @@ import os from 'node:os';
|
|||||||
import path from 'node:path';
|
import path from 'node:path';
|
||||||
import { generateGuardScript, type GuardScriptOptions } from './guard-template.js';
|
import { generateGuardScript, type GuardScriptOptions } from './guard-template.js';
|
||||||
import { generateHookScript, type HookScriptOptions } from './hook-template.js';
|
import { generateHookScript, type HookScriptOptions } from './hook-template.js';
|
||||||
|
import { MAX_CONFIG_BYTES, MAX_SECRET_BYTES, sameAgentId, samePath } from './common.js';
|
||||||
import { inspectPhysicalDirectory, readPhysicalFile } from './managed-files.js';
|
import { inspectPhysicalDirectory, readPhysicalFile } from './managed-files.js';
|
||||||
import { samePath } from './managed-installation.js';
|
|
||||||
import { parseStrictJsonObject, type JsonObject } from './strict-json.js';
|
import { parseStrictJsonObject, type JsonObject } from './strict-json.js';
|
||||||
|
|
||||||
const MAX_SECRET_BYTES = 64 * 1024;
|
|
||||||
const MAX_CONFIG_BYTES = 512 * 1024;
|
|
||||||
const GUARD_SCRIPT = 'guard.js';
|
const GUARD_SCRIPT = 'guard.js';
|
||||||
const AUDIT_SCRIPT = 'hook.js';
|
const AUDIT_SCRIPT = 'hook.js';
|
||||||
|
|
||||||
@@ -22,10 +20,6 @@ export interface ManagedRuntimeStatusOptions {
|
|||||||
readonly auditOptions?: HookScriptOptions;
|
readonly auditOptions?: HookScriptOptions;
|
||||||
}
|
}
|
||||||
|
|
||||||
function sameAgentId(left: string, right: string): boolean {
|
|
||||||
return process.platform === 'win32' ? left.toLowerCase() === right.toLowerCase() : left === right;
|
|
||||||
}
|
|
||||||
|
|
||||||
function requireString(value: unknown, field: string, configPath: string): string {
|
function requireString(value: unknown, field: string, configPath: string): string {
|
||||||
if (typeof value !== 'string' || value.trim().length === 0) {
|
if (typeof value !== 'string' || value.trim().length === 0) {
|
||||||
throw new Error(`Elydora runtime config ${field} is invalid: ${configPath}`);
|
throw new Error(`Elydora runtime config ${field} is invalid: ${configPath}`);
|
||||||
|
|||||||
@@ -2,14 +2,13 @@ import { randomUUID } from 'node:crypto';
|
|||||||
import fsp from 'node:fs/promises';
|
import fsp from 'node:fs/promises';
|
||||||
import type { FileHandle } from 'node:fs/promises';
|
import type { FileHandle } from 'node:fs/promises';
|
||||||
import path from 'node:path';
|
import path from 'node:path';
|
||||||
|
import { asError, errorMessage, hasCode, MAX_SOURCE_BYTES } from './common.js';
|
||||||
import {
|
import {
|
||||||
inspectPhysicalDirectory,
|
inspectPhysicalDirectory,
|
||||||
readPhysicalFile,
|
readPhysicalFile,
|
||||||
type FileSnapshot,
|
type FileSnapshot,
|
||||||
} from './managed-files.js';
|
} from './managed-files.js';
|
||||||
|
|
||||||
const MAX_SOURCE_BYTES = 2 * 1024 * 1024;
|
|
||||||
|
|
||||||
export interface ManagedChangeSpec {
|
export interface ManagedChangeSpec {
|
||||||
readonly filePath: string;
|
readonly filePath: string;
|
||||||
readonly label: string;
|
readonly label: string;
|
||||||
@@ -60,18 +59,6 @@ interface StagedChange {
|
|||||||
|
|
||||||
export type RenameFile = (source: string, destination: string) => Promise<void>;
|
export type RenameFile = (source: string, destination: string) => Promise<void>;
|
||||||
|
|
||||||
function asError(error: unknown): Error {
|
|
||||||
return error instanceof Error ? error : new Error(String(error));
|
|
||||||
}
|
|
||||||
|
|
||||||
function errorMessage(error: unknown): string {
|
|
||||||
return error instanceof Error ? error.message : String(error);
|
|
||||||
}
|
|
||||||
|
|
||||||
function hasCode(error: unknown, code: string): boolean {
|
|
||||||
return typeof error === 'object' && error !== null && 'code' in error && error.code === code;
|
|
||||||
}
|
|
||||||
|
|
||||||
function targetKey(filePath: string): string {
|
function targetKey(filePath: string): string {
|
||||||
const resolved = path.resolve(filePath);
|
const resolved = path.resolve(filePath);
|
||||||
return process.platform === 'win32' ? resolved.toLowerCase() : resolved;
|
return process.platform === 'win32' ? resolved.toLowerCase() : resolved;
|
||||||
@@ -113,16 +100,20 @@ export async function prepareManagedFileChange(
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
async function assertUnchanged(change: ManagedFileChange, operationName: string): Promise<void> {
|
function sameSnapshot(current: FileSnapshot | undefined, original: FileSnapshot | undefined): boolean {
|
||||||
const current = await readPhysicalFile(change.filePath, change.label, change.maximumBytes);
|
if (!current || !original) return !current && !original;
|
||||||
if ((!current && change.original)
|
return current.contents === original.contents
|
||||||
|| (current && !change.original)
|
&& current.device === original.device
|
||||||
|| (current && change.original && (
|
&& current.inode === original.inode;
|
||||||
current.contents !== change.original.contents
|
}
|
||||||
|| current.device !== change.original.device
|
|
||||||
|| current.inode !== change.original.inode
|
async function assertUnchanged(
|
||||||
))) {
|
target: ManagedFilePrecondition,
|
||||||
throw new Error(`${change.label} changed during ${operationName}: ${change.filePath}`);
|
operationName: string,
|
||||||
|
): Promise<void> {
|
||||||
|
const current = await readPhysicalFile(target.filePath, target.label, target.maximumBytes);
|
||||||
|
if (!sameSnapshot(current, target.original)) {
|
||||||
|
throw new Error(`${target.label} changed during ${operationName}: ${target.filePath}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -131,20 +122,7 @@ async function assertPreconditions(
|
|||||||
operationName: string,
|
operationName: string,
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
for (const condition of transaction.preconditions ?? []) {
|
for (const condition of transaction.preconditions ?? []) {
|
||||||
const current = await readPhysicalFile(
|
await assertUnchanged(condition, operationName);
|
||||||
condition.filePath,
|
|
||||||
condition.label,
|
|
||||||
condition.maximumBytes,
|
|
||||||
);
|
|
||||||
if ((!current && condition.original)
|
|
||||||
|| (current && !condition.original)
|
|
||||||
|| (current && condition.original && (
|
|
||||||
current.contents !== condition.original.contents
|
|
||||||
|| current.device !== condition.original.device
|
|
||||||
|| current.inode !== condition.original.inode
|
|
||||||
))) {
|
|
||||||
throw new Error(`${condition.label} changed during ${operationName}: ${condition.filePath}`);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -285,11 +263,7 @@ async function assertCommittedUnchanged(staged: StagedChange): Promise<void> {
|
|||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const committed = staged.committedSnapshot;
|
if (!current || !sameSnapshot(current, staged.committedSnapshot)) {
|
||||||
if (!current || !committed
|
|
||||||
|| current.contents !== committed.contents
|
|
||||||
|| current.device !== committed.device
|
|
||||||
|| current.inode !== committed.inode) {
|
|
||||||
throw new Error(
|
throw new Error(
|
||||||
`${staged.change.label} changed during transaction recovery: ${staged.change.filePath}`,
|
`${staged.change.label} changed during transaction recovery: ${staged.change.filePath}`,
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import os from 'node:os';
|
import os from 'node:os';
|
||||||
import path from 'node:path';
|
import path from 'node:path';
|
||||||
import type { HookScriptOptions } from './hook-template.js';
|
import type { HookScriptOptions } from './hook-template.js';
|
||||||
import { samePath } from './managed-installation.js';
|
import { samePath } from './common.js';
|
||||||
import { parseStrictJsonObject } from './strict-json.js';
|
import { parseStrictJsonObject } from './strict-json.js';
|
||||||
|
|
||||||
export const AGENT_KEY = 'opencode';
|
export const AGENT_KEY = 'opencode';
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import os from 'node:os';
|
|||||||
import path from 'node:path';
|
import path from 'node:path';
|
||||||
import test from 'node:test';
|
import test from 'node:test';
|
||||||
|
|
||||||
import { generateGuardScript } from '../dist/plugins/hook-template.js';
|
import { generateGuardScript } from '../dist/plugins/guard-template.js';
|
||||||
|
|
||||||
const AGENT_ID = 'agent-1';
|
const AGENT_ID = 'agent-1';
|
||||||
|
|
||||||
|
|||||||
Reference in new issue
Block a user