From b7f14ba34bf1eb43acc67f1a7cf87a86fda554ba Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=9B=B7=E7=94=B5=E8=8A=BD=E8=A1=A3?= Date: Wed, 2 Sep 2026 21:23:57 -0400 Subject: [PATCH] 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 --- src/cli.ts | 3 +- src/plugins/common.ts | 31 ++++++++++++++ src/plugins/hook-template.ts | 3 -- src/plugins/managed-files.ts | 15 +------ src/plugins/managed-installation.ts | 18 +------- src/plugins/managed-runtime-status.ts | 8 +--- src/plugins/managed-transaction.ts | 60 ++++++++------------------- src/plugins/opencode-contract.ts | 2 +- test/guard-template.test.mjs | 2 +- 9 files changed, 55 insertions(+), 87 deletions(-) create mode 100644 src/plugins/common.ts diff --git a/src/cli.ts b/src/cli.ts index 2d62c61..eee1200 100644 --- a/src/cli.ts +++ b/src/cli.ts @@ -13,7 +13,8 @@ import { import { writePrivateFile } from './secure-files.js'; import { SUPPORTED_AGENTS } from './plugins/registry.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 { claudecodePlugin } from './plugins/claudecode.js'; import { cursorPlugin } from './plugins/cursor.js'; diff --git a/src/plugins/common.ts b/src/plugins/common.ts new file mode 100644 index 0000000..b168bbb --- /dev/null +++ b/src/plugins/common.ts @@ -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); +} diff --git a/src/plugins/hook-template.ts b/src/plugins/hook-template.ts index c58fe35..dad5829 100644 --- a/src/plugins/hook-template.ts +++ b/src/plugins/hook-template.ts @@ -1,8 +1,5 @@ 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 { readonly failClosed?: boolean; readonly nativePayload?: boolean; diff --git a/src/plugins/managed-files.ts b/src/plugins/managed-files.ts index 5b47c0e..9fe5325 100644 --- a/src/plugins/managed-files.ts +++ b/src/plugins/managed-files.ts @@ -1,8 +1,7 @@ import { constants } from 'node:fs'; import fsp from 'node:fs/promises'; import type { FileHandle } from 'node:fs/promises'; - -const MAX_SOURCE_BYTES = 2 * 1024 * 1024; +import { asError, errorMessage, hasCode, MAX_SOURCE_BYTES } from './common.js'; export interface FileSnapshot { readonly contents: string; @@ -11,18 +10,6 @@ export interface FileSnapshot { 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( filePath: string, label: string, diff --git a/src/plugins/managed-installation.ts b/src/plugins/managed-installation.ts index c858c17..69065ae 100644 --- a/src/plugins/managed-installation.ts +++ b/src/plugins/managed-installation.ts @@ -2,6 +2,7 @@ import os from 'node:os'; import path from 'node:path'; import { ensurePrivateDirectory, resolvePrivateChildDirectory } from '../runtime-paths.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 { generateHookScript, type HookScriptOptions } from './hook-template.js'; import { @@ -19,9 +20,6 @@ import { } from './managed-transaction.js'; import { parseStrictJsonObject } from './strict-json.js'; -const MAX_SECRET_BYTES = 64 * 1024; -const MAX_CONFIG_BYTES = 512 * 1024; - export interface ManagedRuntimePaths { readonly runtimeRoot: string; readonly agentDirectory: string; @@ -79,14 +77,6 @@ interface RuntimeArtifactLocation { 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 { for (const [field, value] of [ ['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( paths: ManagedRuntimePaths, agentKey: string, diff --git a/src/plugins/managed-runtime-status.ts b/src/plugins/managed-runtime-status.ts index bb31c55..0cc8089 100644 --- a/src/plugins/managed-runtime-status.ts +++ b/src/plugins/managed-runtime-status.ts @@ -2,12 +2,10 @@ import os from 'node:os'; import path from 'node:path'; import { generateGuardScript, type GuardScriptOptions } from './guard-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 { samePath } from './managed-installation.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 AUDIT_SCRIPT = 'hook.js'; @@ -22,10 +20,6 @@ export interface ManagedRuntimeStatusOptions { 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 { if (typeof value !== 'string' || value.trim().length === 0) { throw new Error(`Elydora runtime config ${field} is invalid: ${configPath}`); diff --git a/src/plugins/managed-transaction.ts b/src/plugins/managed-transaction.ts index f395445..61d29d7 100644 --- a/src/plugins/managed-transaction.ts +++ b/src/plugins/managed-transaction.ts @@ -2,14 +2,13 @@ import { randomUUID } from 'node:crypto'; import fsp from 'node:fs/promises'; import type { FileHandle } from 'node:fs/promises'; import path from 'node:path'; +import { asError, errorMessage, hasCode, MAX_SOURCE_BYTES } from './common.js'; import { inspectPhysicalDirectory, readPhysicalFile, type FileSnapshot, } from './managed-files.js'; -const MAX_SOURCE_BYTES = 2 * 1024 * 1024; - export interface ManagedChangeSpec { readonly filePath: string; readonly label: string; @@ -60,18 +59,6 @@ interface StagedChange { export type RenameFile = (source: string, destination: string) => Promise; -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 { const resolved = path.resolve(filePath); return process.platform === 'win32' ? resolved.toLowerCase() : resolved; @@ -113,16 +100,20 @@ export async function prepareManagedFileChange( }; } -async function assertUnchanged(change: ManagedFileChange, operationName: string): Promise { - const current = await readPhysicalFile(change.filePath, change.label, change.maximumBytes); - if ((!current && change.original) - || (current && !change.original) - || (current && change.original && ( - current.contents !== change.original.contents - || current.device !== change.original.device - || current.inode !== change.original.inode - ))) { - throw new Error(`${change.label} changed during ${operationName}: ${change.filePath}`); +function sameSnapshot(current: FileSnapshot | undefined, original: FileSnapshot | undefined): boolean { + if (!current || !original) return !current && !original; + return current.contents === original.contents + && current.device === original.device + && current.inode === original.inode; +} + +async function assertUnchanged( + target: ManagedFilePrecondition, + operationName: string, +): Promise { + 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, ): Promise { for (const condition of transaction.preconditions ?? []) { - const current = await readPhysicalFile( - 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}`); - } + await assertUnchanged(condition, operationName); } } @@ -285,11 +263,7 @@ async function assertCommittedUnchanged(staged: StagedChange): Promise { } return; } - const committed = staged.committedSnapshot; - if (!current || !committed - || current.contents !== committed.contents - || current.device !== committed.device - || current.inode !== committed.inode) { + if (!current || !sameSnapshot(current, staged.committedSnapshot)) { throw new Error( `${staged.change.label} changed during transaction recovery: ${staged.change.filePath}`, ); diff --git a/src/plugins/opencode-contract.ts b/src/plugins/opencode-contract.ts index 8d8458f..fa02c9a 100644 --- a/src/plugins/opencode-contract.ts +++ b/src/plugins/opencode-contract.ts @@ -1,7 +1,7 @@ import os from 'node:os'; import path from 'node:path'; import type { HookScriptOptions } from './hook-template.js'; -import { samePath } from './managed-installation.js'; +import { samePath } from './common.js'; import { parseStrictJsonObject } from './strict-json.js'; export const AGENT_KEY = 'opencode'; diff --git a/test/guard-template.test.mjs b/test/guard-template.test.mjs index 62baa22..f43d356 100644 --- a/test/guard-template.test.mjs +++ b/test/guard-template.test.mjs @@ -6,7 +6,7 @@ import os from 'node:os'; import path from 'node:path'; 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';