@phigon/observer 8.1.0 → 8.3.0
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 +244 -18
- package/dist/bin/guardian-runner-darwin-amd64 +0 -0
- package/dist/bin/guardian-runner-darwin-arm64 +0 -0
- package/dist/bin/guardian-runner-linux-amd64 +0 -0
- package/dist/bin/guardian-runner-linux-arm64 +0 -0
- package/dist/bin/guardian-runner-windows-amd64.exe +0 -0
- package/dist/bin/guardian-runner-windows-arm64.exe +0 -0
- package/dist/src/abortable-delay.d.ts +10 -0
- package/dist/src/abortable-delay.d.ts.map +1 -0
- package/dist/src/abortable-delay.js +21 -0
- package/dist/src/abortable-delay.js.map +1 -0
- package/dist/src/action-policy.d.ts.map +1 -1
- package/dist/src/action-policy.js +4 -2
- package/dist/src/action-policy.js.map +1 -1
- package/dist/src/activity/adapters/claude.d.ts.map +1 -1
- package/dist/src/activity/adapters/claude.js +28 -2
- package/dist/src/activity/adapters/claude.js.map +1 -1
- package/dist/src/activity/adapters/cline-task.d.ts +7 -0
- package/dist/src/activity/adapters/cline-task.d.ts.map +1 -0
- package/dist/src/activity/adapters/cline-task.js +197 -0
- package/dist/src/activity/adapters/cline-task.js.map +1 -0
- package/dist/src/activity/adapters/cline.d.ts +3 -0
- package/dist/src/activity/adapters/cline.d.ts.map +1 -0
- package/dist/src/activity/adapters/cline.js +102 -0
- package/dist/src/activity/adapters/cline.js.map +1 -0
- package/dist/src/activity/adapters/codex.d.ts.map +1 -1
- package/dist/src/activity/adapters/codex.js +76 -0
- package/dist/src/activity/adapters/codex.js.map +1 -1
- package/dist/src/activity/adapters/copilot-chat.d.ts +3 -0
- package/dist/src/activity/adapters/copilot-chat.d.ts.map +1 -0
- package/dist/src/activity/adapters/copilot-chat.js +176 -0
- package/dist/src/activity/adapters/copilot-chat.js.map +1 -0
- package/dist/src/activity/adapters/copilot.d.ts +2 -1
- package/dist/src/activity/adapters/copilot.d.ts.map +1 -1
- package/dist/src/activity/adapters/copilot.js +173 -2
- package/dist/src/activity/adapters/copilot.js.map +1 -1
- package/dist/src/activity/adapters/cursor.d.ts +3 -0
- package/dist/src/activity/adapters/cursor.d.ts.map +1 -0
- package/dist/src/activity/adapters/cursor.js +86 -0
- package/dist/src/activity/adapters/cursor.js.map +1 -0
- package/dist/src/activity/adapters/factory-droid.d.ts +3 -0
- package/dist/src/activity/adapters/factory-droid.d.ts.map +1 -0
- package/dist/src/activity/adapters/factory-droid.js +126 -0
- package/dist/src/activity/adapters/factory-droid.js.map +1 -0
- package/dist/src/activity/adapters/gemini.d.ts +3 -0
- package/dist/src/activity/adapters/gemini.d.ts.map +1 -0
- package/dist/src/activity/adapters/gemini.js +177 -0
- package/dist/src/activity/adapters/gemini.js.map +1 -0
- package/dist/src/activity/adapters/goose.d.ts +3 -0
- package/dist/src/activity/adapters/goose.d.ts.map +1 -0
- package/dist/src/activity/adapters/goose.js +116 -0
- package/dist/src/activity/adapters/goose.js.map +1 -0
- package/dist/src/activity/adapters/kiro.d.ts +3 -0
- package/dist/src/activity/adapters/kiro.d.ts.map +1 -0
- package/dist/src/activity/adapters/kiro.js +47 -0
- package/dist/src/activity/adapters/kiro.js.map +1 -0
- package/dist/src/activity/adapters/openai-compatible.d.ts.map +1 -1
- package/dist/src/activity/adapters/openai-compatible.js +9 -1
- package/dist/src/activity/adapters/openai-compatible.js.map +1 -1
- package/dist/src/activity/adapters/opencode.d.ts.map +1 -1
- package/dist/src/activity/adapters/opencode.js +108 -1
- package/dist/src/activity/adapters/opencode.js.map +1 -1
- package/dist/src/activity/adapters/zed.d.ts +3 -0
- package/dist/src/activity/adapters/zed.d.ts.map +1 -0
- package/dist/src/activity/adapters/zed.js +134 -0
- package/dist/src/activity/adapters/zed.js.map +1 -0
- package/dist/src/activity/collector.d.ts +43 -1
- package/dist/src/activity/collector.d.ts.map +1 -1
- package/dist/src/activity/collector.js +490 -77
- package/dist/src/activity/collector.js.map +1 -1
- package/dist/src/activity/goose-sqlite-source.d.ts +33 -0
- package/dist/src/activity/goose-sqlite-source.d.ts.map +1 -0
- package/dist/src/activity/goose-sqlite-source.js +274 -0
- package/dist/src/activity/goose-sqlite-source.js.map +1 -0
- package/dist/src/activity/index.d.ts +3 -0
- package/dist/src/activity/index.d.ts.map +1 -1
- package/dist/src/activity/index.js +3 -0
- package/dist/src/activity/index.js.map +1 -1
- package/dist/src/activity/ingest.d.ts +9 -0
- package/dist/src/activity/ingest.d.ts.map +1 -1
- package/dist/src/activity/ingest.js +94 -4
- package/dist/src/activity/ingest.js.map +1 -1
- package/dist/src/activity/json-document-source.d.ts +151 -0
- package/dist/src/activity/json-document-source.d.ts.map +1 -0
- package/dist/src/activity/json-document-source.js +586 -0
- package/dist/src/activity/json-document-source.js.map +1 -0
- package/dist/src/activity/native-capabilities.d.ts.map +1 -1
- package/dist/src/activity/native-capabilities.js +8 -12
- package/dist/src/activity/native-capabilities.js.map +1 -1
- package/dist/src/activity/normalize.d.ts.map +1 -1
- package/dist/src/activity/normalize.js +64 -6
- package/dist/src/activity/normalize.js.map +1 -1
- package/dist/src/activity/opencode-message-tables.d.ts +87 -0
- package/dist/src/activity/opencode-message-tables.d.ts.map +1 -0
- package/dist/src/activity/opencode-message-tables.js +546 -0
- package/dist/src/activity/opencode-message-tables.js.map +1 -0
- package/dist/src/activity/opencode-sqlite-source.d.ts +7 -0
- package/dist/src/activity/opencode-sqlite-source.d.ts.map +1 -1
- package/dist/src/activity/opencode-sqlite-source.js +154 -63
- package/dist/src/activity/opencode-sqlite-source.js.map +1 -1
- package/dist/src/activity/source-reader.d.ts +60 -4
- package/dist/src/activity/source-reader.d.ts.map +1 -1
- package/dist/src/activity/source-reader.js +593 -75
- package/dist/src/activity/source-reader.js.map +1 -1
- package/dist/src/activity/sqlite-connections.d.ts +77 -0
- package/dist/src/activity/sqlite-connections.d.ts.map +1 -0
- package/dist/src/activity/sqlite-connections.js +220 -0
- package/dist/src/activity/sqlite-connections.js.map +1 -0
- package/dist/src/activity/zed-sqlite-source.d.ts +65 -0
- package/dist/src/activity/zed-sqlite-source.d.ts.map +1 -0
- package/dist/src/activity/zed-sqlite-source.js +547 -0
- package/dist/src/activity/zed-sqlite-source.js.map +1 -0
- package/dist/src/agent-action-executor.d.ts +18 -1
- package/dist/src/agent-action-executor.d.ts.map +1 -1
- package/dist/src/agent-action-executor.js +113 -58
- package/dist/src/agent-action-executor.js.map +1 -1
- package/dist/src/agentic-guardian.d.ts.map +1 -1
- package/dist/src/agentic-guardian.js +6 -5
- package/dist/src/agentic-guardian.js.map +1 -1
- package/dist/src/atomic-replace.d.ts +7 -0
- package/dist/src/atomic-replace.d.ts.map +1 -1
- package/dist/src/atomic-replace.js +28 -3
- package/dist/src/atomic-replace.js.map +1 -1
- package/dist/src/bridge-journal.d.ts +32 -0
- package/dist/src/bridge-journal.d.ts.map +1 -0
- package/dist/src/bridge-journal.js +113 -0
- package/dist/src/bridge-journal.js.map +1 -0
- package/dist/src/cli.d.ts +7 -1
- package/dist/src/cli.d.ts.map +1 -1
- package/dist/src/cli.js +534 -50
- package/dist/src/cli.js.map +1 -1
- package/dist/src/collection-notice.d.ts +34 -0
- package/dist/src/collection-notice.d.ts.map +1 -0
- package/dist/src/collection-notice.js +73 -0
- package/dist/src/collection-notice.js.map +1 -0
- package/dist/src/config.d.ts +39 -0
- package/dist/src/config.d.ts.map +1 -1
- package/dist/src/config.js +187 -36
- package/dist/src/config.js.map +1 -1
- package/dist/src/connection-config.d.ts.map +1 -1
- package/dist/src/connection-config.js +5 -0
- package/dist/src/connection-config.js.map +1 -1
- package/dist/src/content-redaction.d.ts +17 -1
- package/dist/src/content-redaction.d.ts.map +1 -1
- package/dist/src/content-redaction.js +51 -3
- package/dist/src/content-redaction.js.map +1 -1
- package/dist/src/credential-store.d.ts +20 -1
- package/dist/src/credential-store.d.ts.map +1 -1
- package/dist/src/credential-store.js +29 -4
- package/dist/src/credential-store.js.map +1 -1
- package/dist/src/custom-agent-registry.d.ts.map +1 -1
- package/dist/src/custom-agent-registry.js +2 -1
- package/dist/src/custom-agent-registry.js.map +1 -1
- package/dist/src/discovery/adapters/generic.d.ts.map +1 -1
- package/dist/src/discovery/adapters/generic.js +160 -2
- package/dist/src/discovery/adapters/generic.js.map +1 -1
- package/dist/src/discovery/engine.d.ts +7 -1
- package/dist/src/discovery/engine.d.ts.map +1 -1
- package/dist/src/discovery/engine.js +21 -16
- package/dist/src/discovery/engine.js.map +1 -1
- package/dist/src/discovery/known-agent.d.ts +7 -0
- package/dist/src/discovery/known-agent.d.ts.map +1 -1
- package/dist/src/discovery/known-agent.js +9 -4
- package/dist/src/discovery/known-agent.js.map +1 -1
- package/dist/src/discovery/system-evidence.d.ts +40 -1
- package/dist/src/discovery/system-evidence.d.ts.map +1 -1
- package/dist/src/discovery/system-evidence.js +189 -16
- package/dist/src/discovery/system-evidence.js.map +1 -1
- package/dist/src/doctor-check.d.ts +51 -0
- package/dist/src/doctor-check.d.ts.map +1 -0
- package/dist/src/doctor-check.js +174 -0
- package/dist/src/doctor-check.js.map +1 -0
- package/dist/src/enroll.d.ts +86 -0
- package/dist/src/enroll.d.ts.map +1 -0
- package/dist/src/enroll.js +245 -0
- package/dist/src/enroll.js.map +1 -0
- package/dist/src/entry.d.ts +3 -0
- package/dist/src/entry.d.ts.map +1 -0
- package/dist/src/entry.js +14 -0
- package/dist/src/entry.js.map +1 -0
- package/dist/src/event-queue.d.ts +13 -1
- package/dist/src/event-queue.d.ts.map +1 -1
- package/dist/src/event-queue.js +137 -42
- package/dist/src/event-queue.js.map +1 -1
- package/dist/src/event-spool.d.ts +61 -3
- package/dist/src/event-spool.d.ts.map +1 -1
- package/dist/src/event-spool.js +341 -44
- package/dist/src/event-spool.js.map +1 -1
- package/dist/src/generated/phigon/v1/identity_pb.d.ts +7 -0
- package/dist/src/generated/phigon/v1/identity_pb.d.ts.map +1 -1
- package/dist/src/generated/phigon/v1/identity_pb.js +1 -1
- package/dist/src/generated/phigon/v1/identity_pb.js.map +1 -1
- package/dist/src/guardian-containment.d.ts +2 -6
- package/dist/src/guardian-containment.d.ts.map +1 -1
- package/dist/src/guardian-containment.js +12 -63
- package/dist/src/guardian-containment.js.map +1 -1
- package/dist/src/guardian-decision-journal.d.ts.map +1 -1
- package/dist/src/guardian-decision-journal.js +4 -19
- package/dist/src/guardian-decision-journal.js.map +1 -1
- package/dist/src/guardian-managed-exec.d.ts.map +1 -1
- package/dist/src/guardian-managed-exec.js +6 -18
- package/dist/src/guardian-managed-exec.js.map +1 -1
- package/dist/src/guardian-model-bridge.d.ts.map +1 -1
- package/dist/src/guardian-model-bridge.js +2 -1
- package/dist/src/guardian-model-bridge.js.map +1 -1
- package/dist/src/guardian-native.d.ts +0 -1
- package/dist/src/guardian-native.d.ts.map +1 -1
- package/dist/src/guardian-native.js +0 -6
- package/dist/src/guardian-native.js.map +1 -1
- package/dist/src/guardian-process-monitor.d.ts +4 -0
- package/dist/src/guardian-process-monitor.d.ts.map +1 -1
- package/dist/src/guardian-process-monitor.js +82 -26
- package/dist/src/guardian-process-monitor.js.map +1 -1
- package/dist/src/health.d.ts +19 -1
- package/dist/src/health.d.ts.map +1 -1
- package/dist/src/health.js +22 -1
- package/dist/src/health.js.map +1 -1
- package/dist/src/historical-identity-store.d.ts +8 -1
- package/dist/src/historical-identity-store.d.ts.map +1 -1
- package/dist/src/historical-identity-store.js +28 -10
- package/dist/src/historical-identity-store.js.map +1 -1
- package/dist/src/index.d.ts +2 -0
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/index.js +2 -0
- package/dist/src/index.js.map +1 -1
- package/dist/src/installation.d.ts +2 -0
- package/dist/src/installation.d.ts.map +1 -1
- package/dist/src/installation.js +6 -3
- package/dist/src/installation.js.map +1 -1
- package/dist/src/instance-lock.d.ts +45 -0
- package/dist/src/instance-lock.d.ts.map +1 -0
- package/dist/src/instance-lock.js +243 -0
- package/dist/src/instance-lock.js.map +1 -0
- package/dist/src/machine-command-executor.d.ts +22 -7
- package/dist/src/machine-command-executor.d.ts.map +1 -1
- package/dist/src/machine-command-executor.js +80 -19
- package/dist/src/machine-command-executor.js.map +1 -1
- package/dist/src/node-version.d.ts +8 -0
- package/dist/src/node-version.d.ts.map +1 -0
- package/dist/src/node-version.js +17 -0
- package/dist/src/node-version.js.map +1 -0
- package/dist/src/observer-log.d.ts +50 -0
- package/dist/src/observer-log.d.ts.map +1 -0
- package/dist/src/observer-log.js +141 -0
- package/dist/src/observer-log.js.map +1 -0
- package/dist/src/pair.d.ts +68 -1
- package/dist/src/pair.d.ts.map +1 -1
- package/dist/src/pair.js +204 -65
- package/dist/src/pair.js.map +1 -1
- package/dist/src/private-directory.d.ts +10 -0
- package/dist/src/private-directory.d.ts.map +1 -0
- package/dist/src/private-directory.js +66 -0
- package/dist/src/private-directory.js.map +1 -0
- package/dist/src/process-tree.d.ts +73 -0
- package/dist/src/process-tree.d.ts.map +1 -0
- package/dist/src/process-tree.js +256 -0
- package/dist/src/process-tree.js.map +1 -0
- package/dist/src/proxy.d.ts +68 -0
- package/dist/src/proxy.d.ts.map +1 -0
- package/dist/src/proxy.js +209 -0
- package/dist/src/proxy.js.map +1 -0
- package/dist/src/purge.d.ts +17 -0
- package/dist/src/purge.d.ts.map +1 -0
- package/dist/src/purge.js +84 -0
- package/dist/src/purge.js.map +1 -0
- package/dist/src/remove-machine.d.ts +5 -1
- package/dist/src/remove-machine.d.ts.map +1 -1
- package/dist/src/remove-machine.js +28 -14
- package/dist/src/remove-machine.js.map +1 -1
- package/dist/src/server-authority.d.ts +29 -0
- package/dist/src/server-authority.d.ts.map +1 -0
- package/dist/src/server-authority.js +106 -0
- package/dist/src/server-authority.js.map +1 -0
- package/dist/src/server-clock.d.ts +32 -0
- package/dist/src/server-clock.d.ts.map +1 -0
- package/dist/src/server-clock.js +72 -0
- package/dist/src/server-clock.js.map +1 -0
- package/dist/src/service-command.d.ts +20 -0
- package/dist/src/service-command.d.ts.map +1 -0
- package/dist/src/service-command.js +136 -0
- package/dist/src/service-command.js.map +1 -0
- package/dist/src/service.d.ts +98 -0
- package/dist/src/service.d.ts.map +1 -0
- package/dist/src/service.js +472 -0
- package/dist/src/service.js.map +1 -0
- package/dist/src/stream-client.d.ts +21 -1
- package/dist/src/stream-client.d.ts.map +1 -1
- package/dist/src/stream-client.js +137 -24
- package/dist/src/stream-client.js.map +1 -1
- package/dist/src/stream-state.d.ts +2 -0
- package/dist/src/stream-state.d.ts.map +1 -1
- package/dist/src/stream-state.js +5 -2
- package/dist/src/stream-state.js.map +1 -1
- package/dist/src/system-inspection.d.ts +78 -0
- package/dist/src/system-inspection.d.ts.map +1 -0
- package/dist/src/system-inspection.js +281 -0
- package/dist/src/system-inspection.js.map +1 -0
- package/dist/src/update.d.ts.map +1 -1
- package/dist/src/update.js +9 -3
- package/dist/src/update.js.map +1 -1
- package/dist/src/user-notices.d.ts +64 -0
- package/dist/src/user-notices.d.ts.map +1 -0
- package/dist/src/user-notices.js +141 -0
- package/dist/src/user-notices.js.map +1 -0
- package/package.json +7 -7
- package/dist/src/windows-process.d.ts +0 -7
- package/dist/src/windows-process.d.ts.map +0 -1
- package/dist/src/windows-process.js +0 -18
- package/dist/src/windows-process.js.map +0 -1
package/dist/src/cli.js
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { create, toJsonString } from "@bufbuild/protobuf";
|
|
3
3
|
import { timestampDate, timestampFromDate } from "@bufbuild/protobuf/wkt";
|
|
4
|
-
import { constants, realpathSync } from "node:fs";
|
|
4
|
+
import { constants, readFileSync, realpathSync } from "node:fs";
|
|
5
5
|
import { access, readFile } from "node:fs/promises";
|
|
6
|
+
import { dirname, resolve } from "node:path";
|
|
6
7
|
import { createInterface } from "node:readline";
|
|
7
8
|
import { fileURLToPath } from "node:url";
|
|
8
9
|
import { FileActionPolicyStore, managedActionNames, } from "./action-policy.js";
|
|
@@ -10,7 +11,7 @@ import { GuardianDecisionUnavailableError, HTTPGuardianDecisionClient, readGuard
|
|
|
10
11
|
import { AgentActionExecutor } from "./agent-action-executor.js";
|
|
11
12
|
import { ActivityCollector, historySyncQueueHeadroom, mergeInventoryWithSourceIdentities, registeredAgentActivitySources, } from "./activity/collector.js";
|
|
12
13
|
import { ingestActivityJSONL } from "./activity/ingest.js";
|
|
13
|
-
import { loadConfig, DEFAULT_CONTROL_PLANE_ORIGIN, } from "./config.js";
|
|
14
|
+
import { consentedCollectionPolicy, contentOutsideProfileMessage, dataDirectoryFrom, loadConfig, DEFAULT_CONTROL_PLANE_ORIGIN, } from "./config.js";
|
|
14
15
|
import { FileCredentialStore } from "./credential-store.js";
|
|
15
16
|
import { CustomAgentRegistry, customAgentRegistration, } from "./custom-agent-registry.js";
|
|
16
17
|
import { DiscoveryEngine, runDiscoveryLoop, } from "./discovery/index.js";
|
|
@@ -18,9 +19,16 @@ import { AgentState } from "./generated/phigon/v1/common_pb.js";
|
|
|
18
19
|
import { AgentInventorySchema } from "./generated/phigon/v1/identity_pb.js";
|
|
19
20
|
import { HistorySyncResultSchema, HistorySyncResultState, } from "./generated/phigon/v1/history_sync_pb.js";
|
|
20
21
|
import { HistoricalIdentityStore, mergeInventoryWithHistoricalIdentities, } from "./historical-identity-store.js";
|
|
21
|
-
import { createHealthSnapshot, } from "./health.js";
|
|
22
|
+
import { consentHealth, createHealthSnapshot, describeConsent, } from "./health.js";
|
|
22
23
|
import { OBSERVER_CONSENT_POLICY_VERSION, pairMachine, } from "./pair.js";
|
|
24
|
+
import { EnrollmentError, checkedEnrollmentCode, enrollMachine, enrollmentCodeVariable, } from "./enroll.js";
|
|
23
25
|
import { removeMachine } from "./remove-machine.js";
|
|
26
|
+
import { forgetServerAuthority, parseAuthorityFingerprint, pinServerAuthority, trustAuthority } from "./server-authority.js";
|
|
27
|
+
import { readServiceSettings, startService, stopService, uninstallService } from "./service.js";
|
|
28
|
+
import { runServiceCommand, serviceContext } from "./service-command.js";
|
|
29
|
+
import { purgeDataDirectory, removeEmptyDataDirectory } from "./purge.js";
|
|
30
|
+
import { hasByteHeadroom } from "./event-queue.js";
|
|
31
|
+
import { InstanceLock, ObserverAlreadyRunningError, processIsAlive } from "./instance-lock.js";
|
|
24
32
|
import { clearEventSpool } from "./event-spool.js";
|
|
25
33
|
import { GuardianDecisionJournal } from "./guardian-decision-journal.js";
|
|
26
34
|
import { GuardianContainmentExecutor, } from "./guardian-containment.js";
|
|
@@ -34,15 +42,29 @@ import { semanticDecision } from "./agentic-guardian.js";
|
|
|
34
42
|
import { guardianRunnerPath } from "./guardian-native.js";
|
|
35
43
|
import { GuardianProcessMonitor } from "./guardian-process-monitor.js";
|
|
36
44
|
import { setTimeout as delay } from "node:timers/promises";
|
|
45
|
+
import { ServerClock, describeClockSkew } from "./server-clock.js";
|
|
37
46
|
import { StreamStateStore } from "./stream-state.js";
|
|
38
47
|
import { connectionEnvironment } from "./connection-config.js";
|
|
39
48
|
import { latestObserverVersion, newerVersion, offerObserverUpdate } from "./update.js";
|
|
49
|
+
import { UserNotices } from "./user-notices.js";
|
|
50
|
+
import { announceCompanyCollection } from "./collection-notice.js";
|
|
51
|
+
import { defaultMaxDeliveryAgeMinutes, formatDoctorCheck, runDoctorCheck, } from "./doctor-check.js";
|
|
52
|
+
import { proxyForUrl, proxySettings, useProxyFromEnvironment, } from "./proxy.js";
|
|
53
|
+
import { RotatingLogFile, levelledOutput, logLevelFrom, } from "./observer-log.js";
|
|
40
54
|
const defaultOutput = {
|
|
41
55
|
stdout: (value) => process.stdout.write(value),
|
|
42
56
|
stderr: (value) => process.stderr.write(value),
|
|
43
57
|
};
|
|
44
|
-
export async function run(argv, environment = process.env, output = defaultOutput, input = process.stdin) {
|
|
58
|
+
export async function run(argv, environment = process.env, output = defaultOutput, input = process.stdin, dependencies = {}) {
|
|
45
59
|
const [command, ...flags] = argv;
|
|
60
|
+
let logged;
|
|
61
|
+
// A background service runs `run --service-settings <file>`; the file
|
|
62
|
+
// sits in the data directory and holds the settings it was installed with.
|
|
63
|
+
const serviceSettingsFile = command === "run" ? flagValue(flags, "--service-settings") : undefined;
|
|
64
|
+
const ownEnvironment = environment === process.env;
|
|
65
|
+
if (serviceSettingsFile !== undefined) {
|
|
66
|
+
environment = { ...environment, PHIGON_DATA_DIR: dirname(resolve(serviceSettingsFile)) };
|
|
67
|
+
}
|
|
46
68
|
if (command === "--version" || command === "-v" || command === "version") {
|
|
47
69
|
output.stdout(`${await readVersion()}\n`);
|
|
48
70
|
return 0;
|
|
@@ -61,17 +83,51 @@ export async function run(argv, environment = process.env, output = defaultOutpu
|
|
|
61
83
|
command !== "pair" &&
|
|
62
84
|
command !== "remove-machine" &&
|
|
63
85
|
command !== "unpair" &&
|
|
86
|
+
command !== "service" &&
|
|
64
87
|
command !== "run") {
|
|
65
88
|
output.stderr(`Unknown command: ${command}\n\n${helpText()}`);
|
|
66
89
|
return 2;
|
|
67
90
|
}
|
|
91
|
+
if (command === "run") {
|
|
92
|
+
// A background service throws away what it prints, so `run` also keeps
|
|
93
|
+
// a log file in the data directory, at PHIGON_LOG_LEVEL.
|
|
94
|
+
const log = new RotatingLogFile(dataDirectoryFrom(environment), {
|
|
95
|
+
onFailure: (message) => output.stderr(message),
|
|
96
|
+
});
|
|
97
|
+
logged = levelledOutput(output, logLevelFrom(environment["PHIGON_LOG_LEVEL"]), log);
|
|
98
|
+
output = logged;
|
|
99
|
+
}
|
|
100
|
+
let callerEnvironment = environment;
|
|
101
|
+
let proxy;
|
|
68
102
|
try {
|
|
103
|
+
if (serviceSettingsFile !== undefined) {
|
|
104
|
+
const settings = await readServiceSettings(serviceSettingsFile);
|
|
105
|
+
environment = { ...environment, ...settings, PHIGON_DATA_DIR: dirname(resolve(serviceSettingsFile)) };
|
|
106
|
+
if (ownEnvironment) {
|
|
107
|
+
// PATH (to find agents) and the proxy variables (for grpc-js) are
|
|
108
|
+
// read from the process itself.
|
|
109
|
+
Object.assign(process.env, settings);
|
|
110
|
+
}
|
|
111
|
+
callerEnvironment = environment;
|
|
112
|
+
trustExtraCertificates(settings["NODE_EXTRA_CA_CERTS"], output);
|
|
113
|
+
}
|
|
69
114
|
environment = command === "update" ? { PHIGON_DATA_DIR: environment["PHIGON_DATA_DIR"] } : await connectionEnvironment(environment, command === "pair" || command === "run" || command === "doctor" ? flags : []);
|
|
70
115
|
const config = loadConfig(observabilityEnvironment(command, flags, environment));
|
|
116
|
+
// Every HTTPS call and the telemetry stream go through the proxy that
|
|
117
|
+
// HTTPS_PROXY, HTTP_PROXY and NO_PROXY name, when there is one.
|
|
118
|
+
proxy = useProxyFromEnvironment(callerEnvironment, { streamUrl: config.grpcUrl });
|
|
119
|
+
if (proxy.notice !== undefined)
|
|
120
|
+
output.stderr(`${proxy.notice}\n`);
|
|
71
121
|
const version = await readVersion();
|
|
72
122
|
const json = flags.includes("--json");
|
|
73
|
-
|
|
74
|
-
|
|
123
|
+
// The company's code a device management script enrolls with. It never
|
|
124
|
+
// prompts, so it is never offered an update either.
|
|
125
|
+
const enrollmentCode = command === "pair" ? enrollmentCodeFrom(flags, callerEnvironment) : undefined;
|
|
126
|
+
// A background service has a console under conhost on Windows, but nobody
|
|
127
|
+
// reads it: it never prompts.
|
|
128
|
+
const interactive = input === process.stdin && process.stdin.isTTY === true && process.stdout.isTTY === true && !json &&
|
|
129
|
+
serviceSettingsFile === undefined;
|
|
130
|
+
if (command === "update" || (interactive && ((command === "pair" && enrollmentCode === undefined) || command === "run") && environment["PHIGON_UPDATE_CHECK"] !== "false")) {
|
|
75
131
|
const latest = await latestObserverVersion(config.dataDirectory, { force: command === "update" });
|
|
76
132
|
if (command === "update" && (json || flags.includes("--check"))) {
|
|
77
133
|
output.stdout(`${JSON.stringify({ current: version, latest, updateAvailable: latest !== null && newerVersion(version, latest), status: latest === null ? "unavailable" : "checked" })}\n`);
|
|
@@ -88,6 +144,9 @@ export async function run(argv, environment = process.env, output = defaultOutpu
|
|
|
88
144
|
}
|
|
89
145
|
}
|
|
90
146
|
const customAgentRegistry = new CustomAgentRegistry(config.dataDirectory);
|
|
147
|
+
if (command === "service") {
|
|
148
|
+
return await runServiceCommand(flags, config, callerEnvironment, output, json, dependencies);
|
|
149
|
+
}
|
|
91
150
|
if (command === "guardian") {
|
|
92
151
|
const subcommand = flags[0] ?? "status";
|
|
93
152
|
if (subcommand === "exec") {
|
|
@@ -118,6 +177,8 @@ export async function run(argv, environment = process.env, output = defaultOutpu
|
|
|
118
177
|
const action = parseManagedAction(flagValue(flags, "--action"));
|
|
119
178
|
const resource = flagValue(flags, "--resource");
|
|
120
179
|
const agentID = flagValue(flags, "--agent") ?? "";
|
|
180
|
+
if (agentID !== "")
|
|
181
|
+
validatedAgentID(agentID);
|
|
121
182
|
const processID = parseOptionalProcessID(flagValue(flags, "--process-id"));
|
|
122
183
|
const processStartedAt = flagValue(flags, "--process-started-at");
|
|
123
184
|
if ((processID === undefined) !== (processStartedAt === undefined)) {
|
|
@@ -343,6 +404,9 @@ export async function run(argv, environment = process.env, output = defaultOutpu
|
|
|
343
404
|
return result.records > 0 ? 0 : 1;
|
|
344
405
|
}
|
|
345
406
|
if (command === "remove-machine" || command === "unpair") {
|
|
407
|
+
if (flags.includes("--purge")) {
|
|
408
|
+
return await removeMachineAndPurge(config, callerEnvironment, flags, json, output, input, dependencies);
|
|
409
|
+
}
|
|
346
410
|
const credentialStore = new FileCredentialStore(config.dataDirectory);
|
|
347
411
|
const credential = await credentialStore.load();
|
|
348
412
|
if (credential === null) {
|
|
@@ -351,21 +415,42 @@ export async function run(argv, environment = process.env, output = defaultOutpu
|
|
|
351
415
|
if (json && !flags.includes("--yes")) {
|
|
352
416
|
throw new Error("JSON machine removal requires --yes after reviewing the removal scope.");
|
|
353
417
|
}
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
418
|
+
// A running observer would keep writing the queue this removes, then
|
|
419
|
+
// stop as unpaired and be restarted by its service over and over.
|
|
420
|
+
let removalLock;
|
|
421
|
+
try {
|
|
422
|
+
removalLock = await InstanceLock.acquire(config.dataDirectory);
|
|
423
|
+
}
|
|
424
|
+
catch (error) {
|
|
425
|
+
if (!(error instanceof ObserverAlreadyRunningError))
|
|
426
|
+
throw error;
|
|
427
|
+
throw new Error("A Phigon observer is running with this data directory. Stop it, " +
|
|
428
|
+
"or stop its service (phigon-observer service uninstall), and then " +
|
|
429
|
+
"run remove-machine again; remove-machine --purge stops the service " +
|
|
430
|
+
"itself. No request was sent.");
|
|
431
|
+
}
|
|
432
|
+
let result;
|
|
433
|
+
try {
|
|
434
|
+
if (!flags.includes("--yes")) {
|
|
435
|
+
output.stdout(removeMachinePrompt(credential.machineId));
|
|
436
|
+
const confirmed = await readConfirmation(input);
|
|
437
|
+
if (confirmed !== "remove") {
|
|
438
|
+
throw new Error("Machine removal was not confirmed. No request was sent.");
|
|
439
|
+
}
|
|
359
440
|
}
|
|
441
|
+
result = await removeMachine({
|
|
442
|
+
config,
|
|
443
|
+
credential,
|
|
444
|
+
clearCredential: async () => {
|
|
445
|
+
clearEventSpool(config.dataDirectory);
|
|
446
|
+
await forgetServerAuthority(config.dataDirectory);
|
|
447
|
+
return await credentialStore.clear();
|
|
448
|
+
},
|
|
449
|
+
});
|
|
450
|
+
}
|
|
451
|
+
finally {
|
|
452
|
+
await removalLock.release();
|
|
360
453
|
}
|
|
361
|
-
const result = await removeMachine({
|
|
362
|
-
config,
|
|
363
|
-
credential,
|
|
364
|
-
clearCredential: async () => {
|
|
365
|
-
clearEventSpool(config.dataDirectory);
|
|
366
|
-
return await credentialStore.clear();
|
|
367
|
-
},
|
|
368
|
-
});
|
|
369
454
|
output.stdout(json
|
|
370
455
|
? `${JSON.stringify(result, null, 2)}\n`
|
|
371
456
|
: formatRemoveMachineResult(result));
|
|
@@ -386,6 +471,22 @@ export async function run(argv, environment = process.env, output = defaultOutpu
|
|
|
386
471
|
const stop = () => controller.abort();
|
|
387
472
|
process.once("SIGINT", stop);
|
|
388
473
|
process.once("SIGTERM", stop);
|
|
474
|
+
// On Windows the service runs the observer under conhost, and ending
|
|
475
|
+
// the scheduled task ends only conhost; stop with it.
|
|
476
|
+
// A service's console can disappear before the observer has stopped;
|
|
477
|
+
// writing to it must not end the process, the log keeps the message.
|
|
478
|
+
const ignoreConsoleError = () => undefined;
|
|
479
|
+
if (serviceSettingsFile !== undefined) {
|
|
480
|
+
process.stdout.on("error", ignoreConsoleError);
|
|
481
|
+
process.stderr.on("error", ignoreConsoleError);
|
|
482
|
+
}
|
|
483
|
+
const stopWatchingParent = serviceSettingsFile !== undefined && process.platform === "win32"
|
|
484
|
+
? watchParentProcess(process.ppid, () => {
|
|
485
|
+
output.stdout("The background service stopped, so the observer stops too.\n");
|
|
486
|
+
controller.abort();
|
|
487
|
+
})
|
|
488
|
+
: undefined;
|
|
489
|
+
let instanceLock;
|
|
389
490
|
try {
|
|
390
491
|
const credential = await new FileCredentialStore(config.dataDirectory).load();
|
|
391
492
|
if (credential === null) {
|
|
@@ -404,7 +505,15 @@ export async function run(argv, environment = process.env, output = defaultOutpu
|
|
|
404
505
|
credential.consentProfile !== "full") {
|
|
405
506
|
throw new Error("Full-content collection is outside the consented metadata profile.");
|
|
406
507
|
}
|
|
407
|
-
|
|
508
|
+
if (config.contentSettingsOutsideProfile.length > 0) {
|
|
509
|
+
throw new Error(contentOutsideProfileMessage(config.contentSettingsOutsideProfile));
|
|
510
|
+
}
|
|
511
|
+
// Before any queued activity, cursor or saved state is read. A
|
|
512
|
+
// signal only aborts the tasks, so `finally` releases the lock.
|
|
513
|
+
instanceLock = await InstanceLock.acquire(config.dataDirectory);
|
|
514
|
+
logged?.debug(`Observer ${version} started for machine ${credential.machineId} ` +
|
|
515
|
+
`with the ${config.observabilityProfile} profile, using ${config.dataDirectory}.\n`);
|
|
516
|
+
let effectivePolicy = intersectCollectionPolicy(config.policy, credential.consentProfile);
|
|
408
517
|
let collector;
|
|
409
518
|
let resolveCollector;
|
|
410
519
|
const collectorReady = new Promise((resolve) => {
|
|
@@ -414,16 +523,24 @@ export async function run(argv, environment = process.env, output = defaultOutpu
|
|
|
414
523
|
const discoveryEngine = new DiscoveryEngine({
|
|
415
524
|
customAgentRegistry,
|
|
416
525
|
});
|
|
526
|
+
const userNotices = new UserNotices({ enabled: config.userNotices });
|
|
527
|
+
const serverClock = new ServerClock({
|
|
528
|
+
onLargeSkew: (offsetMs) => output.stdout(`${describeClockSkew(offsetMs)}\n`),
|
|
529
|
+
});
|
|
417
530
|
const actionExecutor = new AgentActionExecutor({
|
|
531
|
+
notices: userNotices,
|
|
418
532
|
enabled: config.remoteActionsEnabled,
|
|
419
533
|
commandsEnabled: config.remoteCommandsEnabled,
|
|
420
534
|
machineID: credential.machineId,
|
|
421
535
|
dataDirectory: config.dataDirectory,
|
|
422
536
|
inventory: async () => (await discoveryEngine.discover(credential.machineId)).inventory,
|
|
537
|
+
serverNow: () => serverClock.now(),
|
|
423
538
|
});
|
|
424
539
|
const client = new ObserverStreamClient({
|
|
425
540
|
config,
|
|
426
541
|
actionExecutor,
|
|
542
|
+
serverClock,
|
|
543
|
+
observerVersion: version,
|
|
427
544
|
onHistorySync: async (syncCommand) => {
|
|
428
545
|
const rejected = (detail) => create(HistorySyncResultSchema, {
|
|
429
546
|
jobId: syncCommand.jobId,
|
|
@@ -445,7 +562,7 @@ export async function run(argv, environment = process.env, output = defaultOutpu
|
|
|
445
562
|
return rejected("Workspace collection policy does not allow readable prompts, replies, and tool activity.");
|
|
446
563
|
}
|
|
447
564
|
const expiresAt = timestampDate(syncCommand.expiresAt).getTime();
|
|
448
|
-
const remainingMs = expiresAt -
|
|
565
|
+
const remainingMs = expiresAt - serverClock.now();
|
|
449
566
|
if (!Number.isFinite(remainingMs) || remainingMs <= 0) {
|
|
450
567
|
return rejected("History sync command expired before execution.");
|
|
451
568
|
}
|
|
@@ -468,7 +585,7 @@ export async function run(argv, environment = process.env, output = defaultOutpu
|
|
|
468
585
|
},
|
|
469
586
|
onStatus: (message) => output.stdout(`${message}\n`),
|
|
470
587
|
onPolicy: (policy) => {
|
|
471
|
-
effectivePolicy = intersectCollectionPolicy(config.policy, policy);
|
|
588
|
+
effectivePolicy = intersectCollectionPolicy(config.policy, credential.consentProfile, policy);
|
|
472
589
|
collector?.updatePolicy(effectivePolicy);
|
|
473
590
|
},
|
|
474
591
|
onCursorReset: (acknowledgedSequence, allocationSequence) => {
|
|
@@ -491,11 +608,16 @@ export async function run(argv, environment = process.env, output = defaultOutpu
|
|
|
491
608
|
const historicalStore = new HistoricalIdentityStore(config.dataDirectory, {
|
|
492
609
|
tenantId: credential.tenantId,
|
|
493
610
|
machineId: credential.machineId,
|
|
611
|
+
}, {
|
|
612
|
+
onSaveError: (error) => output.stderr(`Saved agent identities could not be written (${(error instanceof Error ? error.message : String(error))
|
|
613
|
+
.replace(/\s+/gu, " ")
|
|
614
|
+
.slice(0, 200)}); retrying on the next pass.\n`),
|
|
494
615
|
});
|
|
495
616
|
let historicalIdentities = await historicalStore.load();
|
|
496
617
|
const initialDiscovery = await discoveryEngine.discover(credential.machineId);
|
|
497
618
|
historicalIdentities = await historicalStore.remember(initialDiscovery.inventory.agents);
|
|
498
619
|
let latestDiscoveryInventory = initialDiscovery.inventory;
|
|
620
|
+
actionExecutor.observeInventory(latestDiscoveryInventory);
|
|
499
621
|
const publishInventory = () => {
|
|
500
622
|
const withHistory = mergeInventoryWithHistoricalIdentities(latestDiscoveryInventory, historicalIdentities);
|
|
501
623
|
client.updateInventory(mergeInventoryWithSourceIdentities(withHistory, collector?.sourceIdentities() ?? []));
|
|
@@ -505,10 +627,13 @@ export async function run(argv, environment = process.env, output = defaultOutpu
|
|
|
505
627
|
const streaming = client.run(controller.signal);
|
|
506
628
|
const discovering = runDiscoveryLoop(discoveryEngine, credential.machineId, config.discoveryIntervalMs, controller.signal, async (inventory) => {
|
|
507
629
|
latestDiscoveryInventory = inventory;
|
|
630
|
+
actionExecutor.observeInventory(inventory);
|
|
508
631
|
collector?.updateInventory(inventory);
|
|
509
632
|
historicalIdentities = await historicalStore.remember(inventory.agents);
|
|
510
633
|
publishInventory();
|
|
511
|
-
})
|
|
634
|
+
}, (error) => output.stderr(`Agent discovery failed and will retry on the next pass: ${(error instanceof Error ? error.message : String(error))
|
|
635
|
+
.replace(/\s+/gu, " ")
|
|
636
|
+
.slice(0, 200)}\n`));
|
|
512
637
|
const processMonitor = new GuardianProcessMonitor({
|
|
513
638
|
coverageClient: new GuardianHostedClient({ controlPlaneURL: config.controlPlaneUrl, credential: credential.credential }),
|
|
514
639
|
machineId: credential.machineId,
|
|
@@ -523,10 +648,23 @@ export async function run(argv, environment = process.env, output = defaultOutpu
|
|
|
523
648
|
inventory: async () => (await discoveryEngine.discover(credential.machineId)).inventory,
|
|
524
649
|
}),
|
|
525
650
|
journal: new GuardianDecisionJournal(config.dataDirectory),
|
|
651
|
+
notices: userNotices,
|
|
526
652
|
onStatus: (message) => output.stdout(`${message}\n`),
|
|
527
653
|
});
|
|
654
|
+
// Being stopped before the stream first connects is a normal stop.
|
|
655
|
+
const streamReady = async () => {
|
|
656
|
+
try {
|
|
657
|
+
return await client.waitUntilReady(controller.signal);
|
|
658
|
+
}
|
|
659
|
+
catch (error) {
|
|
660
|
+
if (controller.signal.aborted)
|
|
661
|
+
return undefined;
|
|
662
|
+
throw error;
|
|
663
|
+
}
|
|
664
|
+
};
|
|
528
665
|
const monitoring = (async () => {
|
|
529
|
-
await
|
|
666
|
+
if (await streamReady() === undefined)
|
|
667
|
+
return;
|
|
530
668
|
while (!controller.signal.aborted) {
|
|
531
669
|
try {
|
|
532
670
|
await processMonitor.review(latestDiscoveryInventory, controller.signal);
|
|
@@ -543,24 +681,47 @@ export async function run(argv, environment = process.env, output = defaultOutpu
|
|
|
543
681
|
}
|
|
544
682
|
})();
|
|
545
683
|
const collecting = (async () => {
|
|
546
|
-
const allocationSequence = await
|
|
684
|
+
const allocationSequence = await streamReady();
|
|
685
|
+
if (allocationSequence === undefined)
|
|
686
|
+
return;
|
|
687
|
+
// Collection starts now. Where the company consented, tell the
|
|
688
|
+
// person at the computer; a notice never holds collection up.
|
|
689
|
+
void announceCompanyCollection({
|
|
690
|
+
dataDirectory: config.dataDirectory,
|
|
691
|
+
credential,
|
|
692
|
+
profile: config.observabilityProfile,
|
|
693
|
+
notices: userNotices,
|
|
694
|
+
}).then((outcome) => {
|
|
695
|
+
if (outcome === "shown")
|
|
696
|
+
output.stdout("Told the person at this computer that Phigon is recording AI agent activity.\n");
|
|
697
|
+
}, () => output.stderr("The start-of-collection notice could not be remembered; it is shown again on the next start.\n"));
|
|
547
698
|
collector = new ActivityCollector({
|
|
548
699
|
config,
|
|
549
700
|
credential,
|
|
550
701
|
initialSequence: allocationSequence,
|
|
551
702
|
initialAcknowledgedSequence: client.metrics().lastAcknowledgedSequence,
|
|
552
703
|
durableDelivery: true,
|
|
704
|
+
flushDelivery: () => client.flush(),
|
|
553
705
|
policy: effectivePolicy,
|
|
554
706
|
emit: (event) => client.enqueue(event),
|
|
707
|
+
// The spool also has a byte budget; reaching it fails delivery
|
|
708
|
+
// instead of waiting, so both stop well before it.
|
|
555
709
|
canCollect: () => {
|
|
556
710
|
const queue = client.metrics().queue;
|
|
557
|
-
return (queue
|
|
558
|
-
|
|
711
|
+
return (hasByteHeadroom(queue) &&
|
|
712
|
+
queue.capacity - queue.depth >=
|
|
713
|
+
Math.max(32, config.activityRateLimitPerMinute));
|
|
559
714
|
},
|
|
560
715
|
canSyncHistory: () => {
|
|
561
716
|
const queue = client.metrics().queue;
|
|
562
|
-
return
|
|
717
|
+
return hasByteHeadroom(queue) &&
|
|
718
|
+
queue.capacity - queue.depth >= Math.min(queue.capacity, historySyncQueueHeadroom);
|
|
563
719
|
},
|
|
720
|
+
onDeliveryRefused: (error) => output.stderr(`Telemetry delivery is paused (${(error instanceof Error ? error.message : String(error))
|
|
721
|
+
.replace(/\s+/gu, " ")
|
|
722
|
+
.slice(0, 200)}). Collection resumes from the same records once queued events are acknowledged.\n`),
|
|
723
|
+
onSourceFailure: (failure) => output.stderr(`Activity source ${failure.adapterId} could not be read (${failure.message}). ` +
|
|
724
|
+
"It is retried shortly; other sources keep collecting.\n"),
|
|
564
725
|
additionalSources: async () => registeredAgentActivitySources(await customAgentRegistry.list()),
|
|
565
726
|
onSourceIdentity: async (identity) => {
|
|
566
727
|
// Make the source target addressable immediately, then durably
|
|
@@ -623,15 +784,36 @@ export async function run(argv, environment = process.env, output = defaultOutpu
|
|
|
623
784
|
}
|
|
624
785
|
}
|
|
625
786
|
})();
|
|
626
|
-
|
|
787
|
+
try {
|
|
788
|
+
await settleObserverTasks([streaming, discovering, collecting, monitoring, maintaining], controller, () => fatalPersistenceError);
|
|
789
|
+
}
|
|
790
|
+
finally {
|
|
791
|
+
// Write what the spool still buffers. Nothing depends on it:
|
|
792
|
+
// checkpoints only moved past written events, and a lost
|
|
793
|
+
// acknowledgement only sends an event again.
|
|
794
|
+
try {
|
|
795
|
+
client.flush();
|
|
796
|
+
}
|
|
797
|
+
catch { /* Read again on the next start. */ }
|
|
798
|
+
}
|
|
627
799
|
output.stdout("Observer stopped.\n");
|
|
628
800
|
}
|
|
629
801
|
finally {
|
|
630
802
|
process.removeListener("SIGINT", stop);
|
|
631
803
|
process.removeListener("SIGTERM", stop);
|
|
804
|
+
stopWatchingParent?.();
|
|
805
|
+
process.stdout.removeListener("error", ignoreConsoleError);
|
|
806
|
+
process.stderr.removeListener("error", ignoreConsoleError);
|
|
807
|
+
await instanceLock?.release();
|
|
632
808
|
}
|
|
633
809
|
return 0;
|
|
634
810
|
}
|
|
811
|
+
if (command === "pair" && enrollmentCode !== undefined) {
|
|
812
|
+
if (config.controlPlaneUrl.origin !== DEFAULT_CONTROL_PLANE_ORIGIN && !config.grpcEndpointConfigured) {
|
|
813
|
+
throw new Error("Custom workspaces require --grpc-url alongside --control-plane-url (or PHIGON_GRPC_URL). Copy both endpoints from your workspace's Connect machine dialog.");
|
|
814
|
+
}
|
|
815
|
+
return await enrollWithCode(enrollmentCode, flags, config, callerEnvironment, json, version, output);
|
|
816
|
+
}
|
|
635
817
|
if (command === "pair") {
|
|
636
818
|
if (config.controlPlaneUrl.origin !== DEFAULT_CONTROL_PLANE_ORIGIN && !config.grpcEndpointConfigured) {
|
|
637
819
|
throw new Error("Custom workspaces require --grpc-url alongside --control-plane-url (or PHIGON_GRPC_URL). Copy both endpoints from your workspace's Connect machine dialog.");
|
|
@@ -642,6 +824,8 @@ export async function run(argv, environment = process.env, output = defaultOutpu
|
|
|
642
824
|
}
|
|
643
825
|
const displayName = flagValue(flags, "--name");
|
|
644
826
|
const consentProfile = parseConsentProfile(flagValue(flags, "--consent-profile"));
|
|
827
|
+
const authorityFlag = flagValue(flags, "--server-ca-sha256");
|
|
828
|
+
const authority = authorityFlag === undefined ? undefined : parseAuthorityFingerprint(authorityFlag);
|
|
645
829
|
if (json && !flags.includes("--accept-consent")) {
|
|
646
830
|
throw new Error("JSON pairing requires --accept-consent after reviewing the selected profile.");
|
|
647
831
|
}
|
|
@@ -652,17 +836,41 @@ export async function run(argv, environment = process.env, output = defaultOutpu
|
|
|
652
836
|
throw new Error("Consent was not granted. No pairing request was sent.");
|
|
653
837
|
}
|
|
654
838
|
}
|
|
655
|
-
|
|
656
|
-
config,
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
839
|
+
if (authority !== undefined) {
|
|
840
|
+
await pinServerAuthority(config.controlPlaneUrl, authority, config.dataDirectory, undefined, {
|
|
841
|
+
proxy: proxyForUrl(config.controlPlaneUrl, proxySettings(callerEnvironment)),
|
|
842
|
+
});
|
|
843
|
+
}
|
|
844
|
+
let result;
|
|
845
|
+
try {
|
|
846
|
+
result = await pairMachine({
|
|
847
|
+
config,
|
|
848
|
+
code,
|
|
849
|
+
consentProfile,
|
|
850
|
+
...(displayName === undefined ? {} : { displayName }),
|
|
851
|
+
});
|
|
852
|
+
}
|
|
853
|
+
catch (error) {
|
|
854
|
+
// An authority pinned for a pairing that failed is not kept.
|
|
855
|
+
if (authority !== undefined && await new FileCredentialStore(config.dataDirectory).load() === null) {
|
|
856
|
+
await forgetServerAuthority(config.dataDirectory);
|
|
857
|
+
}
|
|
858
|
+
throw error;
|
|
859
|
+
}
|
|
661
860
|
output.stdout(json
|
|
662
861
|
? `${JSON.stringify(result, null, 2)}\n`
|
|
663
862
|
: formatPairResult(result));
|
|
664
863
|
return 0;
|
|
665
864
|
}
|
|
865
|
+
if (flags.includes("--check")) {
|
|
866
|
+
const report = await runDoctorCheck({
|
|
867
|
+
config,
|
|
868
|
+
observerVersion: version,
|
|
869
|
+
maxDeliveryAgeMinutes: parseMaxDeliveryAge(flagValue(flags, "--max-delivery-age")),
|
|
870
|
+
});
|
|
871
|
+
output.stdout(json ? `${JSON.stringify(report, null, 2)}\n` : formatDoctorCheck(report));
|
|
872
|
+
return report.exitCode;
|
|
873
|
+
}
|
|
666
874
|
const storedCredential = await new FileCredentialStore(config.dataDirectory).load();
|
|
667
875
|
let streamCursor;
|
|
668
876
|
let streamCursorIssue;
|
|
@@ -682,15 +890,107 @@ export async function run(argv, environment = process.env, output = defaultOutpu
|
|
|
682
890
|
const health = createHealthSnapshot({
|
|
683
891
|
...config,
|
|
684
892
|
machineTokenConfigured: config.machineTokenConfigured || storedCredential !== null,
|
|
685
|
-
}, version, new Date(), streamCursor, streamCursorIssue);
|
|
893
|
+
}, version, new Date(), streamCursor, streamCursorIssue, storedCredential === null ? undefined : consentHealth(storedCredential));
|
|
686
894
|
output.stdout(json ? `${JSON.stringify(health, null, 2)}\n` : formatHealth(health));
|
|
687
895
|
return 0;
|
|
688
896
|
}
|
|
689
897
|
catch (error) {
|
|
690
898
|
const message = error instanceof Error ? error.message : "Unknown error.";
|
|
691
|
-
output.stderr(`Observer command failed: ${message}\n`);
|
|
899
|
+
(logged?.error ?? output.stderr)(`Observer command failed: ${message}\n`);
|
|
900
|
+
if (error instanceof EnrollmentError) {
|
|
901
|
+
if (flags.includes("--json")) {
|
|
902
|
+
output.stdout(`${JSON.stringify({
|
|
903
|
+
enrolled: false,
|
|
904
|
+
error: { code: error.code, message: error.message },
|
|
905
|
+
exitCode: error.exitCode,
|
|
906
|
+
}, null, 2)}\n`);
|
|
907
|
+
}
|
|
908
|
+
return error.exitCode;
|
|
909
|
+
}
|
|
692
910
|
return 1;
|
|
693
911
|
}
|
|
912
|
+
finally {
|
|
913
|
+
proxy?.restore();
|
|
914
|
+
}
|
|
915
|
+
}
|
|
916
|
+
/**
|
|
917
|
+
* The enrollment code from --enrollment-code or PHIGON_ENROLLMENT_CODE, or
|
|
918
|
+
* undefined for an owner's pairing. The value is never repeated in errors.
|
|
919
|
+
*/
|
|
920
|
+
function enrollmentCodeFrom(flags, environment) {
|
|
921
|
+
const flagGiven = flags.some((flag) => flag === "--enrollment-code" || flag.startsWith("--enrollment-code="));
|
|
922
|
+
const fromFlag = flagValue(flags, "--enrollment-code");
|
|
923
|
+
if (flagGiven && (fromFlag === undefined || fromFlag.trim() === "")) {
|
|
924
|
+
throw new Error(`--enrollment-code needs the code. Device management scripts should set ${enrollmentCodeVariable} instead, which keeps it out of process lists.`);
|
|
925
|
+
}
|
|
926
|
+
const fromEnvironment = environment[enrollmentCodeVariable]?.trim() || undefined;
|
|
927
|
+
const code = fromFlag ?? fromEnvironment;
|
|
928
|
+
if (code === undefined)
|
|
929
|
+
return undefined;
|
|
930
|
+
if (flags.some((flag) => flag === "--code" || flag.startsWith("--code="))) {
|
|
931
|
+
throw new Error(`Use either --code with a one-time pairing code, or an enrollment code (--enrollment-code or ${enrollmentCodeVariable}), not both.`);
|
|
932
|
+
}
|
|
933
|
+
if (flags.some((flag) => flag === "--accept-consent" || flag === "--consent-profile" || flag.startsWith("--consent-profile="))) {
|
|
934
|
+
throw new Error("With an enrollment code your company has chosen the collection profile and given consent. Remove --consent-profile and --accept-consent.");
|
|
935
|
+
}
|
|
936
|
+
return code;
|
|
937
|
+
}
|
|
938
|
+
/**
|
|
939
|
+
* `pair` with an enrollment code: no prompt, the company's consent, and a
|
|
940
|
+
* distinct exit code for each failure a device management script can act on.
|
|
941
|
+
*/
|
|
942
|
+
async function enrollWithCode(code, flags, config, environment, json, version, output) {
|
|
943
|
+
// Checked before anything is pinned or sent.
|
|
944
|
+
const enrollmentCode = checkedEnrollmentCode(code);
|
|
945
|
+
const displayName = flagValue(flags, "--name");
|
|
946
|
+
const authorityFlag = flagValue(flags, "--server-ca-sha256");
|
|
947
|
+
const authority = authorityFlag === undefined ? undefined : parseAuthorityFingerprint(authorityFlag);
|
|
948
|
+
if (authority !== undefined) {
|
|
949
|
+
await pinServerAuthority(config.controlPlaneUrl, authority, config.dataDirectory, undefined, {
|
|
950
|
+
proxy: proxyForUrl(config.controlPlaneUrl, proxySettings(environment)),
|
|
951
|
+
});
|
|
952
|
+
}
|
|
953
|
+
let result;
|
|
954
|
+
try {
|
|
955
|
+
result = await enrollMachine({
|
|
956
|
+
config,
|
|
957
|
+
code: enrollmentCode,
|
|
958
|
+
observerVersion: version,
|
|
959
|
+
...(displayName === undefined ? {} : { displayName }),
|
|
960
|
+
onRetry: (waitMs) => output.stderr(`The Phigon server is busy. Trying again in ${Math.ceil(waitMs / 1_000)} seconds.\n`),
|
|
961
|
+
});
|
|
962
|
+
}
|
|
963
|
+
catch (error) {
|
|
964
|
+
// An authority pinned for an enrollment that failed is not kept.
|
|
965
|
+
if (authority !== undefined && await new FileCredentialStore(config.dataDirectory).load() === null) {
|
|
966
|
+
await forgetServerAuthority(config.dataDirectory);
|
|
967
|
+
}
|
|
968
|
+
throw error;
|
|
969
|
+
}
|
|
970
|
+
const warning = config.observabilityProfile === "full" && result.consent.profile !== "full"
|
|
971
|
+
? "Your company's code covers the metadata profile only, so the observer does not start with PHIGON_OBSERVABILITY_PROFILE=full. Set it to metadata, or ask for a full-activity code."
|
|
972
|
+
: undefined;
|
|
973
|
+
if (warning !== undefined)
|
|
974
|
+
output.stderr(`${warning}\n`);
|
|
975
|
+
output.stdout(json ? `${JSON.stringify({ enrolled: true, ...result }, null, 2)}\n` : formatEnrollResult(result));
|
|
976
|
+
return 0;
|
|
977
|
+
}
|
|
978
|
+
function formatEnrollResult(result) {
|
|
979
|
+
return [
|
|
980
|
+
"Computer enrolled.",
|
|
981
|
+
`Machine: ${result.displayName} (${result.machineId})`,
|
|
982
|
+
`Control plane: ${result.controlPlaneOrigin}`,
|
|
983
|
+
`Consent: given by your company${result.consent.enrollmentCodeLabel === undefined ? "" : ` with the enrollment code "${result.consent.enrollmentCodeLabel}"`}, ${result.consent.profile} profile`,
|
|
984
|
+
"Machine credential: stored securely",
|
|
985
|
+
...(result.replacedMachineId === undefined
|
|
986
|
+
? []
|
|
987
|
+
: [
|
|
988
|
+
`Earlier machine: ${result.replacedMachineId} (the workspace no longer accepted its key)`,
|
|
989
|
+
"This computer was enrolled as a new machine. Activity the earlier machine had not delivered was moved to the retired-pairings folder in the data directory.",
|
|
990
|
+
]),
|
|
991
|
+
"Next step: phigon-observer service install",
|
|
992
|
+
"",
|
|
993
|
+
].join("\n");
|
|
694
994
|
}
|
|
695
995
|
function guardianAgentMessage(action, allowed, approvalRequired, reason, adminContract, approvalState) {
|
|
696
996
|
const operation = action.replace(".", " ");
|
|
@@ -745,6 +1045,7 @@ async function runGuardianLaunch(flags, environment, output, input, dataDirector
|
|
|
745
1045
|
if (agentID === undefined) {
|
|
746
1046
|
throw new Error("guardian launch requires --agent <agent-id>.");
|
|
747
1047
|
}
|
|
1048
|
+
validatedAgentID(agentID);
|
|
748
1049
|
const model = validatedModelResource(flagValue(gatewayFlags, "--model"));
|
|
749
1050
|
const executable = flags[separator + 1];
|
|
750
1051
|
const commandArguments = flags.slice(separator + 2);
|
|
@@ -754,6 +1055,14 @@ async function runGuardianLaunch(flags, environment, output, input, dataDirector
|
|
|
754
1055
|
}
|
|
755
1056
|
return await runGuardedCommand(executable, commandArguments, gatewayFlags, model, environment, output, dataDirectory);
|
|
756
1057
|
}
|
|
1058
|
+
// The control plane refuses any other agent ID with a generic manifest error,
|
|
1059
|
+
// so check here and say what is wrong.
|
|
1060
|
+
function validatedAgentID(value) {
|
|
1061
|
+
if (!/^[A-Za-z0-9._:-]{1,128}$/u.test(value)) {
|
|
1062
|
+
throw new Error("--agent must be 1-128 letters, numbers, dots, underscores, colons or hyphens.");
|
|
1063
|
+
}
|
|
1064
|
+
return value;
|
|
1065
|
+
}
|
|
757
1066
|
function validateGuardianExecFlags(flags) {
|
|
758
1067
|
const valued = new Set([
|
|
759
1068
|
"--agent",
|
|
@@ -806,7 +1115,7 @@ async function runGuardedCommand(executable, arguments_, flags, model, environme
|
|
|
806
1115
|
output.stdout("Phigon Guardian blocked execution: administrator policy is unavailable.\n");
|
|
807
1116
|
return 77;
|
|
808
1117
|
}
|
|
809
|
-
const agentId = flagValue(flags, "--agent") ?? "managed-agent";
|
|
1118
|
+
const agentId = validatedAgentID(flagValue(flags, "--agent") ?? "managed-agent");
|
|
810
1119
|
const context = await readGuardianActionContext(flagValue(flags, "--context-file"));
|
|
811
1120
|
const hosted = new GuardianHostedClient({ controlPlaneURL: config.controlPlaneUrl, credential: credential.credential });
|
|
812
1121
|
const childEnvironment = executionEnvironment({ ...process.env, ...environment });
|
|
@@ -910,6 +1219,37 @@ async function runGuardedCommand(executable, arguments_, flags, model, environme
|
|
|
910
1219
|
await bridge?.close();
|
|
911
1220
|
}
|
|
912
1221
|
}
|
|
1222
|
+
/**
|
|
1223
|
+
* Calls `onGone` once the process `pid` has exited, checking every few
|
|
1224
|
+
* seconds. Returns a function that stops watching.
|
|
1225
|
+
*/
|
|
1226
|
+
export function watchParentProcess(pid, onGone, intervalMs = 5_000, isAlive = processIsAlive) {
|
|
1227
|
+
const timer = setInterval(() => {
|
|
1228
|
+
if (pid > 0 && isAlive(pid))
|
|
1229
|
+
return;
|
|
1230
|
+
clearInterval(timer);
|
|
1231
|
+
onGone();
|
|
1232
|
+
}, intervalMs);
|
|
1233
|
+
timer.unref();
|
|
1234
|
+
return () => clearInterval(timer);
|
|
1235
|
+
}
|
|
1236
|
+
/**
|
|
1237
|
+
* NODE_EXTRA_CA_CERTS from the service settings. Node.js reads the variable
|
|
1238
|
+
* only when it starts, so the service adds those authorities itself.
|
|
1239
|
+
*/
|
|
1240
|
+
function trustExtraCertificates(file, output) {
|
|
1241
|
+
if (file === undefined || file === "")
|
|
1242
|
+
return;
|
|
1243
|
+
try {
|
|
1244
|
+
const certificates = readFileSync(file, "utf8")
|
|
1245
|
+
.match(/-----BEGIN CERTIFICATE-----[\s\S]+?-----END CERTIFICATE-----/gu) ?? [];
|
|
1246
|
+
for (const certificate of certificates)
|
|
1247
|
+
trustAuthority(`${certificate}\n`);
|
|
1248
|
+
}
|
|
1249
|
+
catch {
|
|
1250
|
+
output.stderr(`The certificates in NODE_EXTRA_CA_CERTS (${file}) could not be read; HTTPS uses the system's authorities only.\n`);
|
|
1251
|
+
}
|
|
1252
|
+
}
|
|
913
1253
|
export async function settleObserverTasks(tasks, controller, fatalError) {
|
|
914
1254
|
try {
|
|
915
1255
|
await Promise.all(tasks);
|
|
@@ -922,7 +1262,8 @@ export async function settleObserverTasks(tasks, controller, fatalError) {
|
|
|
922
1262
|
await Promise.allSettled(tasks);
|
|
923
1263
|
}
|
|
924
1264
|
}
|
|
925
|
-
function intersectCollectionPolicy(
|
|
1265
|
+
function intersectCollectionPolicy(configured, consentProfile, server) {
|
|
1266
|
+
const local = consentedCollectionPolicy(configured, consentProfile);
|
|
926
1267
|
return Object.freeze({
|
|
927
1268
|
promptContent: local.promptContent &&
|
|
928
1269
|
server?.promptContent === true,
|
|
@@ -989,16 +1330,22 @@ function helpText() {
|
|
|
989
1330
|
" phigon-observer agent list [--json]",
|
|
990
1331
|
" phigon-observer agent remove --id <id> [--yes] [--json]",
|
|
991
1332
|
" phigon-observer doctor [--profile <metadata|full>] [--json]",
|
|
1333
|
+
" phigon-observer doctor --check [--max-delivery-age <minutes>] [--json]",
|
|
992
1334
|
" phigon-observer update [--check] [--yes] [--json]",
|
|
993
1335
|
" phigon-observer --version",
|
|
994
1336
|
" phigon-observer discover [--json]",
|
|
995
1337
|
" phigon-observer ingest --agent <agent-id|custom> [--name <agent-name>] [--quiet] [--json]",
|
|
996
|
-
" phigon-observer pair --code <one-time-code> --consent-profile <metadata|full> [--control-plane-url <url> --grpc-url <url>] [--name <machine-name>] [--accept-consent] [--json]",
|
|
997
|
-
" phigon-observer
|
|
1338
|
+
" phigon-observer pair --code <one-time-code> --consent-profile <metadata|full> [--control-plane-url <url> --grpc-url <url> [--server-ca-sha256 <fingerprint>]] [--name <machine-name>] [--accept-consent] [--json]",
|
|
1339
|
+
" PHIGON_ENROLLMENT_CODE=<code> phigon-observer pair [--control-plane-url <url> --grpc-url <url> [--server-ca-sha256 <fingerprint>]] [--name <machine-name>] [--json]",
|
|
1340
|
+
" phigon-observer pair --enrollment-code <code> [same options]",
|
|
1341
|
+
" phigon-observer remove-machine [--purge] [--yes] [--json]",
|
|
998
1342
|
" phigon-observer run [--profile <metadata|full>]",
|
|
1343
|
+
" phigon-observer service install [--profile <metadata|full>] [--json]",
|
|
1344
|
+
" phigon-observer service uninstall [--json]",
|
|
1345
|
+
" phigon-observer service status [--json]",
|
|
999
1346
|
" phigon-observer help",
|
|
1000
1347
|
"",
|
|
1001
|
-
"The doctor command validates local configuration without transmitting data.",
|
|
1348
|
+
"The doctor command validates local configuration without transmitting data. With --check it also probes the control plane and exits non-zero when something needs attention: 3 not paired, 4 consent missing or outdated, 5 observer not running, 6 nothing delivered within --max-delivery-age minutes (default 15), 7 control plane unreachable.",
|
|
1002
1349
|
"The agent commands manage dynamic local monitoring definitions for custom agents without changing observer code.",
|
|
1003
1350
|
"The authorize-action command is a fail-closed decision point for managed gateways; it does not intercept actions performed outside that boundary. Stop/force-stop requires an administrator policy and exact process identity.",
|
|
1004
1351
|
"The optional Guardian context file supplies bounded task_id, user_prompt, agent_plan, justification, and recent_activity fields for semantic review.",
|
|
@@ -1008,8 +1355,10 @@ function helpText() {
|
|
|
1008
1355
|
"The discover command reports a sanitized one-time local inventory.",
|
|
1009
1356
|
"The ingest command accepts NDJSON on stdin for every supported bridge; use --agent custom --name <agent-name> for any other agent.",
|
|
1010
1357
|
"The pair command shows the selected collection scope, requires machine-owner acceptance, and stores that evidence with the mode-0600 credential.",
|
|
1011
|
-
"
|
|
1358
|
+
"With a company's enrollment code (phe_...), pair asks nothing: the company has given consent and chosen the profile, which the observer stores and keeps collection within. Prefer PHIGON_ENROLLMENT_CODE to --enrollment-code, which shows in process lists. The code is never printed or stored. A busy server is retried a few times. Exit codes: 20 try again later, 21 the code expired, was revoked, is used up or outdated, 22 not a valid code, 23 this computer already used this code, 24 already connected to this workspace. Then run phigon-observer service install.",
|
|
1359
|
+
"The remove-machine command stops collection on this paired machine and revokes its credential; activity already collected stays until an administrator deletes the machine or it expires. It requires local confirmation. With --purge it also removes the background service and every file the observer keeps in the data directory.",
|
|
1012
1360
|
"The run command maintains an authenticated, resumable telemetry stream; --profile applies the same portable local collection profile on Windows, macOS, and Linux.",
|
|
1361
|
+
"The service commands run the observer in the background for the signed-in user (a Task Scheduler task, a LaunchAgent or a systemd user unit) with this data directory and these settings, starting it at sign-in and again if it stops. Install needs a paired computer with the owner's consent.",
|
|
1013
1362
|
"",
|
|
1014
1363
|
].join("\n");
|
|
1015
1364
|
}
|
|
@@ -1059,25 +1408,149 @@ function formatPairResult(result) {
|
|
|
1059
1408
|
`Machine: ${result.displayName} (${result.machineId})`,
|
|
1060
1409
|
`Control plane: ${result.controlPlaneOrigin}`,
|
|
1061
1410
|
"Machine credential: stored securely",
|
|
1411
|
+
...(result.replacedMachineId === undefined
|
|
1412
|
+
? []
|
|
1413
|
+
: [
|
|
1414
|
+
`Earlier machine: ${result.replacedMachineId} (the workspace no longer accepted its key)`,
|
|
1415
|
+
"This computer was paired as a new machine. Activity the earlier machine had not delivered was moved to the retired-pairings folder in the data directory.",
|
|
1416
|
+
]),
|
|
1062
1417
|
"",
|
|
1063
1418
|
].join("\n");
|
|
1064
1419
|
}
|
|
1420
|
+
/**
|
|
1421
|
+
* `remove-machine --purge`: the usual removal, then the background service
|
|
1422
|
+
* and every file the observer keeps on this computer. The company's copy of
|
|
1423
|
+
* the activity is not touched; only an administrator or retention removes it.
|
|
1424
|
+
*/
|
|
1425
|
+
async function removeMachineAndPurge(config, environment, flags, json, output, input, dependencies) {
|
|
1426
|
+
const credentialStore = new FileCredentialStore(config.dataDirectory);
|
|
1427
|
+
const credential = await credentialStore.load();
|
|
1428
|
+
if (json && !flags.includes("--yes")) {
|
|
1429
|
+
throw new Error("JSON machine removal requires --yes after reviewing the removal scope.");
|
|
1430
|
+
}
|
|
1431
|
+
if (!flags.includes("--yes")) {
|
|
1432
|
+
output.stdout(purgePrompt(credential?.machineId, config.dataDirectory));
|
|
1433
|
+
if (await readConfirmation(input) !== "remove") {
|
|
1434
|
+
throw new Error("Machine removal was not confirmed. Nothing was changed.");
|
|
1435
|
+
}
|
|
1436
|
+
}
|
|
1437
|
+
// The service would restart the observer this removes, so stop it first,
|
|
1438
|
+
// and start it again if the removal fails.
|
|
1439
|
+
let service;
|
|
1440
|
+
try {
|
|
1441
|
+
service = serviceContext(config, environment, dependencies);
|
|
1442
|
+
}
|
|
1443
|
+
catch {
|
|
1444
|
+
// No background service on this system; there is nothing to stop.
|
|
1445
|
+
}
|
|
1446
|
+
const serviceStopped = service !== undefined && await stopService(service);
|
|
1447
|
+
const restartService = async () => {
|
|
1448
|
+
if (serviceStopped && service !== undefined)
|
|
1449
|
+
await startService(service).catch(() => undefined);
|
|
1450
|
+
};
|
|
1451
|
+
if (serviceStopped)
|
|
1452
|
+
await waitForObserverToStop(config.dataDirectory, dependencies.stopTimeoutMs ?? 15_000);
|
|
1453
|
+
let removalLock;
|
|
1454
|
+
try {
|
|
1455
|
+
removalLock = await InstanceLock.acquire(config.dataDirectory);
|
|
1456
|
+
}
|
|
1457
|
+
catch (error) {
|
|
1458
|
+
await restartService();
|
|
1459
|
+
if (!(error instanceof ObserverAlreadyRunningError))
|
|
1460
|
+
throw error;
|
|
1461
|
+
throw new Error("A Phigon observer is running with this data directory. Stop it, or stop its service, " +
|
|
1462
|
+
"and then run remove-machine again. No request was sent.");
|
|
1463
|
+
}
|
|
1464
|
+
let result;
|
|
1465
|
+
let purged;
|
|
1466
|
+
try {
|
|
1467
|
+
if (credential !== null) {
|
|
1468
|
+
try {
|
|
1469
|
+
result = await removeMachine({
|
|
1470
|
+
config,
|
|
1471
|
+
credential,
|
|
1472
|
+
clearCredential: async () => {
|
|
1473
|
+
clearEventSpool(config.dataDirectory);
|
|
1474
|
+
await forgetServerAuthority(config.dataDirectory);
|
|
1475
|
+
return await credentialStore.clear();
|
|
1476
|
+
},
|
|
1477
|
+
});
|
|
1478
|
+
}
|
|
1479
|
+
catch (error) {
|
|
1480
|
+
await removalLock.release();
|
|
1481
|
+
await restartService();
|
|
1482
|
+
throw error;
|
|
1483
|
+
}
|
|
1484
|
+
}
|
|
1485
|
+
if (service !== undefined)
|
|
1486
|
+
await uninstallService(service);
|
|
1487
|
+
purged = await purgeDataDirectory(config.dataDirectory, ["observer.lock"]);
|
|
1488
|
+
}
|
|
1489
|
+
finally {
|
|
1490
|
+
await removalLock.release();
|
|
1491
|
+
}
|
|
1492
|
+
const directoryRemoved = purged.leftBehind.length === 0 &&
|
|
1493
|
+
await removeEmptyDataDirectory(config.dataDirectory);
|
|
1494
|
+
const local = {
|
|
1495
|
+
serviceRemoved: serviceStopped,
|
|
1496
|
+
filesRemoved: purged.removed,
|
|
1497
|
+
dataDirectory: config.dataDirectory,
|
|
1498
|
+
dataDirectoryRemoved: directoryRemoved,
|
|
1499
|
+
leftBehind: purged.leftBehind,
|
|
1500
|
+
};
|
|
1501
|
+
output.stdout(json
|
|
1502
|
+
? `${JSON.stringify({ ...(result ?? { status: "not_paired" }), purged: local }, null, 2)}\n`
|
|
1503
|
+
: [
|
|
1504
|
+
...(result === undefined
|
|
1505
|
+
? ["This computer was not paired, so nothing was sent to a workspace."]
|
|
1506
|
+
: [formatRemoveMachineResult(result).trimEnd()]),
|
|
1507
|
+
`Background service: ${serviceStopped ? "removed" : "was not installed"}`,
|
|
1508
|
+
directoryRemoved
|
|
1509
|
+
? `Local data: removed, with ${config.dataDirectory}`
|
|
1510
|
+
: `Local data: removed from ${config.dataDirectory}${purged.leftBehind.length === 0 ? "" : `; ${purged.leftBehind.length} file(s) the observer did not create were left there`}`,
|
|
1511
|
+
"To remove the program too, run: npm uninstall --global @phigon/observer",
|
|
1512
|
+
"",
|
|
1513
|
+
].join("\n"));
|
|
1514
|
+
return 0;
|
|
1515
|
+
}
|
|
1516
|
+
async function waitForObserverToStop(dataDirectory, timeoutMs) {
|
|
1517
|
+
const deadline = Date.now() + timeoutMs;
|
|
1518
|
+
while (Date.now() < deadline && (await InstanceLock.inspect(dataDirectory)).running) {
|
|
1519
|
+
await delay(250);
|
|
1520
|
+
}
|
|
1521
|
+
}
|
|
1522
|
+
function purgePrompt(machineId, dataDirectory) {
|
|
1523
|
+
return [
|
|
1524
|
+
"Remove this computer from Phigon and delete its local data?",
|
|
1525
|
+
...(machineId === undefined ? ["This computer is not paired."] : [
|
|
1526
|
+
`Machine: ${machineId}`,
|
|
1527
|
+
"Collection stops at once and this computer's observer key is revoked.",
|
|
1528
|
+
"Activity already collected stays with your company until an administrator deletes this computer or it expires under the retention policy.",
|
|
1529
|
+
]),
|
|
1530
|
+
"The background service is removed, and every file the observer keeps in",
|
|
1531
|
+
`${dataDirectory} is deleted, including activity not yet delivered.`,
|
|
1532
|
+
'Type "remove" to continue: ',
|
|
1533
|
+
].join("\n");
|
|
1534
|
+
}
|
|
1065
1535
|
function removeMachinePrompt(machineId) {
|
|
1066
1536
|
return [
|
|
1067
1537
|
"Remove this machine from Phigon?",
|
|
1068
1538
|
`Machine: ${machineId}`,
|
|
1069
|
-
"
|
|
1070
|
-
"
|
|
1539
|
+
"Collection stops at once and this computer's observer key is revoked.",
|
|
1540
|
+
"Activity already collected stays with your company until an administrator deletes this computer or it expires under the retention policy.",
|
|
1071
1541
|
'Type "remove" to continue: ',
|
|
1072
1542
|
].join("\n");
|
|
1073
1543
|
}
|
|
1074
1544
|
function formatRemoveMachineResult(result) {
|
|
1075
1545
|
return [
|
|
1076
|
-
|
|
1546
|
+
result.status === "already_removed"
|
|
1547
|
+
? "This machine was already removed from the workspace, or its key was revoked. The local copy is now cleaned up."
|
|
1548
|
+
: "Machine removed successfully.",
|
|
1077
1549
|
`Machine: ${result.machineId}`,
|
|
1078
1550
|
`Control plane: ${result.controlPlaneOrigin}`,
|
|
1551
|
+
"Collection: stopped; the observer key is revoked",
|
|
1079
1552
|
"Machine credential: removed from this device",
|
|
1080
|
-
"
|
|
1553
|
+
"Collected activity: kept until an administrator deletes this computer or it expires",
|
|
1081
1554
|
"Next step: pair again to reconnect this observer.",
|
|
1082
1555
|
"",
|
|
1083
1556
|
].join("\n");
|
|
@@ -1175,7 +1648,8 @@ function flagValue(flags, name) {
|
|
|
1175
1648
|
for (let index = 0; index < flags.length; index++) {
|
|
1176
1649
|
const flag = flags[index];
|
|
1177
1650
|
if (flag === name) {
|
|
1178
|
-
|
|
1651
|
+
const value = flags[index + 1];
|
|
1652
|
+
return value === undefined || value.startsWith("--") ? undefined : value;
|
|
1179
1653
|
}
|
|
1180
1654
|
if (flag?.startsWith(equalsPrefix)) {
|
|
1181
1655
|
return flag.slice(equalsPrefix.length);
|
|
@@ -1204,8 +1678,8 @@ function observabilityEnvironment(command, flags, environment) {
|
|
|
1204
1678
|
const profile = flagValue(flags, "--profile");
|
|
1205
1679
|
if (profile === undefined)
|
|
1206
1680
|
return environment;
|
|
1207
|
-
if (command !== "doctor" && command !== "run") {
|
|
1208
|
-
throw new Error("--profile is supported only by doctor and
|
|
1681
|
+
if (command !== "doctor" && command !== "run" && !(command === "service" && flags[0] === "install")) {
|
|
1682
|
+
throw new Error("--profile is supported only by doctor, run and service install.");
|
|
1209
1683
|
}
|
|
1210
1684
|
if (profile !== "metadata" && profile !== "full") {
|
|
1211
1685
|
throw new Error("--profile must be metadata or full.");
|
|
@@ -1215,6 +1689,15 @@ function observabilityEnvironment(command, flags, environment) {
|
|
|
1215
1689
|
PHIGON_OBSERVABILITY_PROFILE: profile,
|
|
1216
1690
|
};
|
|
1217
1691
|
}
|
|
1692
|
+
function parseMaxDeliveryAge(value) {
|
|
1693
|
+
if (value === undefined)
|
|
1694
|
+
return defaultMaxDeliveryAgeMinutes;
|
|
1695
|
+
const minutes = Number(value);
|
|
1696
|
+
if (!/^\d{1,5}$/u.test(value) || minutes < 1 || minutes > 10_080) {
|
|
1697
|
+
throw new Error("--max-delivery-age must be a number of minutes from 1 to 10080.");
|
|
1698
|
+
}
|
|
1699
|
+
return minutes;
|
|
1700
|
+
}
|
|
1218
1701
|
function parseConsentProfile(value) {
|
|
1219
1702
|
if (value === "metadata" || value === "full")
|
|
1220
1703
|
return value;
|
|
@@ -1257,6 +1740,7 @@ function formatHealth(health) {
|
|
|
1257
1740
|
: [
|
|
1258
1741
|
`Stream cursor: ${health.streamCursor.acknowledgedSequence} for ${health.streamCursor.machineId}`,
|
|
1259
1742
|
]),
|
|
1743
|
+
...(health.consent === undefined ? [] : [`Consent: ${describeConsent(health.consent)}`]),
|
|
1260
1744
|
`Content collection: ${health.safeCollectionDefaults ? "metadata only" : "explicit categories enabled"}`,
|
|
1261
1745
|
"",
|
|
1262
1746
|
].join("\n");
|