diff --git a/docs/agent-skill-sharing-implementation-checklist.md b/docs/agent-skill-sharing-implementation-checklist.md index 52fc49d70e5..00c7225feb8 100644 --- a/docs/agent-skill-sharing-implementation-checklist.md +++ b/docs/agent-skill-sharing-implementation-checklist.md @@ -723,6 +723,11 @@ record before the multi-distro gate. ## 11. Implement and validate SSH targets +The 2026-08-11 live Orca inventory contains only the connected `windows 2` environment and no +non-local worktree or repository. Real SSH validation therefore requires registering an isolated +SSH macOS, Ubuntu 20.04, or supported Windows target; implementation and contract tests continue +without treating the missing external topology as a product failure. + - [x] Define a host-side installer command that invokes the same installer core and structured result contract. - [x] Transfer immutable packages through the existing SSH/SFTP provider into bounded private @@ -766,7 +771,10 @@ record before the multi-distro gate. URLs, upload policies, download grants, and credentials. - [x] Redact sensitive network values before logger invocation, not after ingestion. - [x] Audit authorization and lifecycle events without instruction or script contents. -- [ ] Verify diagnostics and support bundles preserve the same exclusions. +- [x] Verify diagnostics and support bundles preserve the same exclusions. Desktop install spans + map requests and results to bounded labels before the tracer boundary; regression coverage + proves raw paths, filenames, manifests, ACL values, durable share URLs, upload policies, + download grants, credentials, and raw error text never enter the collected bundle. ### Security review diff --git a/src/main/skills/skill-install-request-service.ts b/src/main/skills/skill-install-request-service.ts index 82dd749aaf3..dcd617a0923 100644 --- a/src/main/skills/skill-install-request-service.ts +++ b/src/main/skills/skill-install-request-service.ts @@ -13,6 +13,7 @@ import { downloadSkillPackageGrant } from './skill-package-download' import { detectSkillProvidersInWsl } from './skill-wsl-provider-detection' import { createWslSkillInstallFilesystem } from './skill-wsl-install-filesystem' import { skillInstallFailureFromError } from './skill-install-operation-error' +import { startSkillInstallOperation } from './skill-operation-observability' type StagedSkillPackage = { archivePath: string @@ -68,6 +69,21 @@ export async function executeSkillInstallRequest( dependencies: SkillInstallRequestDependencies ): Promise { const request = SkillInstallRequestSchema.parse(input) + const operation = startSkillInstallOperation(request) + try { + const result = await executeParsedSkillInstallRequest(request, dependencies) + operation.complete(result) + return result + } catch (error) { + operation.fail(error) + throw error + } +} + +async function executeParsedSkillInstallRequest( + request: SkillInstallRequest, + dependencies: SkillInstallRequestDependencies +): Promise { const destination = await resolveSkillInstallDestination( request.destination, dependencies.authority diff --git a/src/main/skills/skill-operation-observability.test.ts b/src/main/skills/skill-operation-observability.test.ts new file mode 100644 index 00000000000..f113832d12d --- /dev/null +++ b/src/main/skills/skill-operation-observability.test.ts @@ -0,0 +1,127 @@ +import { mkdtempSync, rmSync, writeFileSync } from 'node:fs' +import { tmpdir } from 'node:os' +import { join } from 'node:path' +import { afterEach, beforeEach, describe, expect, it } from 'vitest' +import type { SkillInstallRequest, SkillInstallResult } from '../../shared/skill-install-contract' +import { collectBundle } from '../observability/bundle' +import { _resetTracerForTests, setActiveSink, type TracerSink } from '../observability/tracer' +import { startSkillInstallOperation } from './skill-operation-observability' + +type CapturingSink = TracerSink & { records: unknown[] } + +const PRIVATE_VALUES = { + localPath: '/Users/private/team skills/payroll-skill.tar.gz', + canonicalPath: '/Users/private/.agents/skills/payroll', + providerPath: '/Users/private/.codex/skills/payroll', + connectionId: 'private-production-ssh', + skillName: 'payroll-instructions', + filename: 'salary-review.md', + manifest: 'manifest={private-instructions}', + acl: 'acl=private-user-id', + shareUrl: 'orca://skill-share/private-share-id', + uploadPolicy: 'policy=private-signed-upload', + downloadGrant: 'https://storage.googleapis.com/private-bucket/object?X-Goog-Signature=secret', + credential: 'authorization=Bearer private-access-token' +} + +function request(): SkillInstallRequest { + return { + operationId: 'operation-private', + package: { + packageId: 'package-123', + versionId: 'version-456', + packageDigest: 'a'.repeat(64), + archiveSha256: 'b'.repeat(64), + compressedBytes: 1234 + }, + ingress: { kind: 'local-file', path: PRIVATE_VALUES.localPath }, + destination: { + scope: 'global', + executionTarget: { kind: 'ssh', connectionId: PRIVATE_VALUES.connectionId } + } + } +} + +function result(): SkillInstallResult { + return { + operationId: 'operation-private', + status: 'installed', + name: PRIVATE_VALUES.skillName, + packageDigest: 'a'.repeat(64), + canonicalPath: PRIVATE_VALUES.canonicalPath, + placements: [ + { + provider: 'codex', + path: PRIVATE_VALUES.providerPath, + topology: 'provider-alias', + status: 'installed' + } + ] + } +} + +let sink: CapturingSink +let directory: string + +beforeEach(() => { + sink = { + records: [], + push(record) { + this.records.push(record) + }, + flush() {}, + close() {} + } + directory = mkdtempSync(join(tmpdir(), 'orca-skill-observability-')) + setActiveSink(sink) +}) + +afterEach(() => { + _resetTracerForTests() + rmSync(directory, { recursive: true, force: true }) +}) + +describe('skill operation observability', () => { + it('maps install results to bounded labels without private operation data', () => { + const operation = startSkillInstallOperation(request()) + operation.complete(result()) + + const serialized = JSON.stringify(sink.records) + expect(serialized).toContain('package-123') + expect(serialized).toContain('version-456') + expect(serialized).toContain('global-ssh') + expect(serialized).toContain('provider-alias') + for (const value of Object.values(PRIVATE_VALUES)) { + expect(serialized).not.toContain(value) + } + }) + + it('keeps support bundles free of paths, filenames, share URLs, policies, ACLs, and grants', () => { + const operation = startSkillInstallOperation({ + ...request(), + ingress: { + kind: 'download-grant', + url: PRIVATE_VALUES.downloadGrant, + expiresAt: '2030-01-01T00:00:00Z' + } + }) + operation.fail(new Error(Object.values(PRIVATE_VALUES).join(' '))) + const traceFile = join(directory, 'trace.ndjson') + writeFileSync(traceFile, `${sink.records.map((record) => JSON.stringify(record)).join('\n')}\n`) + + const bundle = collectBundle({ + traceFilePath: traceFile, + maxFiles: 1, + appVersion: 'test', + platform: 'darwin', + arch: 'arm64', + osRelease: 'test', + orcaChannel: 'dev' + }) + + expect(bundle.payload).toContain('skill-install-unknown') + for (const value of Object.values(PRIVATE_VALUES)) { + expect(bundle.payload).not.toContain(value) + } + }) +}) diff --git a/src/main/skills/skill-operation-observability.ts b/src/main/skills/skill-operation-observability.ts new file mode 100644 index 00000000000..b2ad7f58348 --- /dev/null +++ b/src/main/skills/skill-operation-observability.ts @@ -0,0 +1,76 @@ +import type { + SkillInstallDestination, + SkillInstallRequest, + SkillInstallResult, + SkillPlacementResult +} from '../../shared/skill-install-contract' +import { startSpan, type ActiveSpan } from '../observability/tracer' +import { skillInstallFailureFromError } from './skill-install-operation-error' + +const SAFE_LABEL = /^[a-z0-9][a-z0-9._-]{0,63}$/i + +export type SkillInstallOperationSpan = { + complete(result: SkillInstallResult): void + fail(error: unknown): void +} + +function safeLabel(value: string | undefined, fallback: string): string { + return value && SAFE_LABEL.test(value) ? value : fallback +} + +function destinationLabel(destination: SkillInstallDestination): string { + if (destination.scope === 'workspace') { + return 'workspace' + } + return `global-${destination.executionTarget?.kind ?? 'host'}` +} + +function recordPlacement(span: ActiveSpan, placement: SkillPlacementResult): void { + span.addEvent('skill.placement', { + provider: safeLabel(placement.provider, 'unknown'), + topology: placement.topology, + status: placement.status, + errorCategory: safeLabel(placement.errorCategory, 'none') + }) +} + +export function startSkillInstallOperation( + request: SkillInstallRequest +): SkillInstallOperationSpan { + const span = startSpan('skill.install', { + attributes: { + packageId: request.package.packageId, + versionId: request.package.versionId, + phase: 'install', + destination: destinationLabel(request.destination), + transport: request.ingress.kind, + compressedBytes: request.package.compressedBytes + } + }) + return { + complete(result) { + span.setAttribute('status', result.status) + span.setAttribute('placementCount', result.placements.length) + for (const placement of result.placements) { + recordPlacement(span, placement) + } + if (result.status === 'cancelled') { + span.interrupt(safeLabel(result.errorCategory, 'skill-install-cancelled')) + } else if (result.status === 'failed') { + span.fail(safeLabel(result.errorCategory, 'skill-install-failed')) + } else { + span.end() + } + }, + fail(error) { + const failure = skillInstallFailureFromError(error) + const code = safeLabel(failure?.code, 'skill-install-unknown') + span.setAttribute('status', failure?.category === 'cancelled' ? 'cancelled' : 'failed') + if (failure?.category === 'cancelled') { + span.interrupt(code) + } else { + span.fail(code) + } + } + } +}