@zerwiz/ymir 0.1.4 → 0.1.5
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/.agents/README.md +291 -0
- package/.agents/agents/bragi-marketer.md +111 -0
- package/.agents/agents/brokk.md +352 -0
- package/.agents/agents/forseti-reviewer.md +93 -0
- package/.agents/agents/frigg-consent.md +77 -0
- package/.agents/agents/groa-update.md +77 -0
- package/.agents/agents/hnoss-designer.md +69 -0
- package/.agents/agents/huginn-researcher.md +99 -0
- package/.agents/agents/jord-projects.md +78 -0
- package/.agents/agents/kvasir-scout.md +91 -0
- package/.agents/agents/mimir-planner.md +92 -0
- package/.agents/agents/muninn-stow.md +77 -0
- package/.agents/agents/saga-bearings.md +78 -0
- package/.agents/agents/sindri-developer.md +114 -0
- package/.agents/agents/snotra-documenter.md +105 -0
- package/.agents/agents/syn-recovery.md +78 -0
- package/.agents/agents/tyr-check.md +78 -0
- package/.agents/agents/urdh-hold.md +77 -0
- package/.agents/agents/volundr-smith.md +80 -0
- package/.agents/agents/vor-diagnostics.md +77 -0
- package/.agents/assets/agents/naming.md +50 -0
- package/.agents/assets/agents/registry.md +93 -0
- package/.agents/assets/agents/runtime.md +36 -0
- package/.agents/assets/agents/toon-tasks-cli.md +42 -0
- package/.agents/assets/templates/company_entity.template.md +37 -0
- package/.agents/backend/backends/cmux.sh +649 -0
- package/.agents/backend/backends/herdr-eventwait.py +157 -0
- package/.agents/backend/backends/herdr-workspace-move.py +114 -0
- package/.agents/backend/backends/herdr.sh +3386 -0
- package/.agents/backend/backends/orca.sh +290 -0
- package/.agents/backend/backends/tmux.sh +357 -0
- package/.agents/backend/backends/zellij.sh +700 -0
- package/.agents/backend/fm-afk-launch.sh +654 -0
- package/.agents/backend/fm-afk-return.sh +241 -0
- package/.agents/backend/fm-afk-start.sh +172 -0
- package/.agents/backend/fm-arm-command-policy.mjs +960 -0
- package/.agents/backend/fm-arm-pretool-check.sh +200 -0
- package/.agents/backend/fm-backend-hometag-lib.sh +52 -0
- package/.agents/backend/fm-backend.sh +981 -0
- package/.agents/backend/fm-backlog-handoff.sh +909 -0
- package/.agents/backend/fm-backlog-receive.sh +187 -0
- package/.agents/backend/fm-backlog-transition-lib.sh +779 -0
- package/.agents/backend/fm-bearings-board.sh +204 -0
- package/.agents/backend/fm-bearings-snapshot.sh +546 -0
- package/.agents/backend/fm-bootstrap.sh +1568 -0
- package/.agents/backend/fm-branch-outcome.sh +203 -0
- package/.agents/backend/fm-branch-prompt.sh +101 -0
- package/.agents/backend/fm-brief.sh +452 -0
- package/.agents/backend/fm-busy-event.sh +234 -0
- package/.agents/backend/fm-busy-lib.sh +986 -0
- package/.agents/backend/fm-captain-hold.sh +1001 -0
- package/.agents/backend/fm-cd-command-policy.mjs +153 -0
- package/.agents/backend/fm-cd-pretool-check.sh +190 -0
- package/.agents/backend/fm-check-lib.sh +74 -0
- package/.agents/backend/fm-check-register.sh +42 -0
- package/.agents/backend/fm-check-unregister.sh +52 -0
- package/.agents/backend/fm-classify-lib.sh +1770 -0
- package/.agents/backend/fm-claude-stop-autoarm.sh +334 -0
- package/.agents/backend/fm-composer-lib.sh +1416 -0
- package/.agents/backend/fm-config-inherit-lib.sh +1202 -0
- package/.agents/backend/fm-config-push.sh +254 -0
- package/.agents/backend/fm-control-lib.sh +251 -0
- package/.agents/backend/fm-control.sh +878 -0
- package/.agents/backend/fm-crew-state.sh +625 -0
- package/.agents/backend/fm-cursor-lib.sh +243 -0
- package/.agents/backend/fm-decision-hold.sh +232 -0
- package/.agents/backend/fm-doc-audience-check.sh +269 -0
- package/.agents/backend/fm-dod-lib.sh +67 -0
- package/.agents/backend/fm-ensure-agents-md.sh +253 -0
- package/.agents/backend/fm-extension-launch-barrier.mjs +129 -0
- package/.agents/backend/fm-extension.mjs +2577 -0
- package/.agents/backend/fm-extension.sh +16 -0
- package/.agents/backend/fm-ff-lib.sh +420 -0
- package/.agents/backend/fm-fleet-snapshot.sh +1463 -0
- package/.agents/backend/fm-fleet-sync.sh +458 -0
- package/.agents/backend/fm-fleet-view.sh +96 -0
- package/.agents/backend/fm-gate-refuse-lib.sh +102 -0
- package/.agents/backend/fm-guard.sh +242 -0
- package/.agents/backend/fm-harness.sh +195 -0
- package/.agents/backend/fm-herdr-ci-cleanup.sh +115 -0
- package/.agents/backend/fm-herdr-lab.sh +346 -0
- package/.agents/backend/fm-herdr-session-cleanup.sh +337 -0
- package/.agents/backend/fm-home-seed.sh +984 -0
- package/.agents/backend/fm-home-summary-refresh.sh +255 -0
- package/.agents/backend/fm-hook-host-lib.sh +36 -0
- package/.agents/backend/fm-inactive-reconcile.sh +531 -0
- package/.agents/backend/fm-inbox.sh +399 -0
- package/.agents/backend/fm-install-actionlint.sh +84 -0
- package/.agents/backend/fm-install-herdr.sh +96 -0
- package/.agents/backend/fm-install-shellcheck.sh +84 -0
- package/.agents/backend/fm-install-treehouse.sh +95 -0
- package/.agents/backend/fm-kimi-turnend-hook.sh +276 -0
- package/.agents/backend/fm-lease-lib.sh +218 -0
- package/.agents/backend/fm-lease.sh +189 -0
- package/.agents/backend/fm-line-cap-lib.sh +51 -0
- package/.agents/backend/fm-lint-workflows.sh +137 -0
- package/.agents/backend/fm-lint.sh +591 -0
- package/.agents/backend/fm-lock-lib.sh +104 -0
- package/.agents/backend/fm-lock.sh +107 -0
- package/.agents/backend/fm-marker-lib.sh +12 -0
- package/.agents/backend/fm-merge-local.sh +74 -0
- package/.agents/backend/fm-merge-outcome-lib.sh +138 -0
- package/.agents/backend/fm-nm-run-lib.sh +124 -0
- package/.agents/backend/fm-on.sh +125 -0
- package/.agents/backend/fm-operational-input.sh +253 -0
- package/.agents/backend/fm-peek.sh +46 -0
- package/.agents/backend/fm-pending-reply-lib.sh +1409 -0
- package/.agents/backend/fm-pr-check.sh +135 -0
- package/.agents/backend/fm-pr-lib.sh +1016 -0
- package/.agents/backend/fm-pr-merge.sh +707 -0
- package/.agents/backend/fm-pr-poll.sh +110 -0
- package/.agents/backend/fm-primary-scope-lib.sh +33 -0
- package/.agents/backend/fm-procevent-extension-capture.pl +259 -0
- package/.agents/backend/fm-procevent-lavish.sh +628 -0
- package/.agents/backend/fm-procevent-lib.sh +892 -0
- package/.agents/backend/fm-procevent-quota.sh +290 -0
- package/.agents/backend/fm-procevent-remote-reply.sh +577 -0
- package/.agents/backend/fm-procevent-when.sh +504 -0
- package/.agents/backend/fm-procevent.sh +1466 -0
- package/.agents/backend/fm-project-mode.sh +92 -0
- package/.agents/backend/fm-project-origin-lib.sh +180 -0
- package/.agents/backend/fm-promote.sh +282 -0
- package/.agents/backend/fm-public-followup-emit.sh +260 -0
- package/.agents/backend/fm-public-followup-lib.sh +398 -0
- package/.agents/backend/fm-public-followup.sh +1330 -0
- package/.agents/backend/fm-push-transition-lib.sh +168 -0
- package/.agents/backend/fm-quota-axi-lib.sh +93 -0
- package/.agents/backend/fm-quota-choose.sh +384 -0
- package/.agents/backend/fm-remote-delta-read.sh +189 -0
- package/.agents/backend/fm-remote-doctor.sh +800 -0
- package/.agents/backend/fm-remote-entrypoint.sh +193 -0
- package/.agents/backend/fm-remote-file.sh +234 -0
- package/.agents/backend/fm-remote-home-provision.sh +259 -0
- package/.agents/backend/fm-remote-home-seed.sh +259 -0
- package/.agents/backend/fm-remote-inherit-push.sh +90 -0
- package/.agents/backend/fm-remote-inherit.sh +179 -0
- package/.agents/backend/fm-remote-job-lib.sh +1241 -0
- package/.agents/backend/fm-remote-job-reap-orphans.sh +137 -0
- package/.agents/backend/fm-remote-job-worker.sh +1124 -0
- package/.agents/backend/fm-remote-readiness-lib.sh +44 -0
- package/.agents/backend/fm-remote-secondmate-control.sh +340 -0
- package/.agents/backend/fm-review-diff.sh +158 -0
- package/.agents/backend/fm-secondmate-charter-lib.sh +44 -0
- package/.agents/backend/fm-secondmate-nudge-lib.sh +68 -0
- package/.agents/backend/fm-secondmate-parent-lib.sh +70 -0
- package/.agents/backend/fm-secondmate-reconcile.sh +371 -0
- package/.agents/backend/fm-secondmate-registry-lib.sh +311 -0
- package/.agents/backend/fm-secondmate-report.sh +88 -0
- package/.agents/backend/fm-send.sh +1059 -0
- package/.agents/backend/fm-session-lock-lib.sh +176 -0
- package/.agents/backend/fm-session-start.sh +970 -0
- package/.agents/backend/fm-sessionstart-cursor.sh +40 -0
- package/.agents/backend/fm-sessionstart-nudge.sh +45 -0
- package/.agents/backend/fm-sessionstart-run.sh +145 -0
- package/.agents/backend/fm-spawn.sh +3153 -0
- package/.agents/backend/fm-startup-memory-budget-lib.sh +224 -0
- package/.agents/backend/fm-startup-memory-budget.sh +94 -0
- package/.agents/backend/fm-startup-network.sh +642 -0
- package/.agents/backend/fm-stow-cascade.sh +251 -0
- package/.agents/backend/fm-subagent-pretool-check.sh +207 -0
- package/.agents/backend/fm-supervise-daemon.sh +1732 -0
- package/.agents/backend/fm-supervision-instructions.sh +215 -0
- package/.agents/backend/fm-supervision-lib.sh +90 -0
- package/.agents/backend/fm-supervisor-target-lib.sh +78 -0
- package/.agents/backend/fm-tangle-lib.sh +53 -0
- package/.agents/backend/fm-task-inbox-lib.sh +402 -0
- package/.agents/backend/fm-tasks-axi-lib.sh +123 -0
- package/.agents/backend/fm-teardown.sh +2922 -0
- package/.agents/backend/fm-test-isolation-proof.sh +588 -0
- package/.agents/backend/fm-test-run.sh +2191 -0
- package/.agents/backend/fm-timeout-lib.sh +141 -0
- package/.agents/backend/fm-timing-lib.sh +179 -0
- package/.agents/backend/fm-tmux-lib.sh +291 -0
- package/.agents/backend/fm-tool-update-check.sh +898 -0
- package/.agents/backend/fm-trace-context-lib.sh +227 -0
- package/.agents/backend/fm-transition-lib.sh +103 -0
- package/.agents/backend/fm-turnend-guard-cursor.sh +391 -0
- package/.agents/backend/fm-turnend-guard-grok.sh +90 -0
- package/.agents/backend/fm-turnend-guard.sh +432 -0
- package/.agents/backend/fm-update.sh +110 -0
- package/.agents/backend/fm-vendor-auth-probe.sh +190 -0
- package/.agents/backend/fm-voice-client.py +1373 -0
- package/.agents/backend/fm-voice-relay.py +1256 -0
- package/.agents/backend/fm-wake-drain.sh +592 -0
- package/.agents/backend/fm-wake-grant.sh +127 -0
- package/.agents/backend/fm-wake-lib.sh +1849 -0
- package/.agents/backend/fm-watch-arm.sh +602 -0
- package/.agents/backend/fm-watch-checkpoint.sh +109 -0
- package/.agents/backend/fm-watch.sh +1962 -0
- package/.agents/backend/fm-x-dismiss.sh +116 -0
- package/.agents/backend/fm-x-followup.sh +286 -0
- package/.agents/backend/fm-x-lib.sh +1000 -0
- package/.agents/backend/fm-x-link.sh +234 -0
- package/.agents/backend/fm-x-poll.sh +185 -0
- package/.agents/backend/fm-x-reply.sh +385 -0
- package/.agents/backend/fm_voice_frame.py +166 -0
- package/.agents/backend/fm_voice_records.py +574 -0
- package/.agents/backend/model-bridge.py +290 -0
- package/.agents/backend/opencode-go-bridge.py +30 -0
- package/.agents/bus/README.md +12 -0
- package/.agents/bus/messages.json +1 -0
- package/.agents/config/agents.machine.example.yaml +16 -0
- package/.agents/config/agents.yaml +50 -0
- package/.agents/config/agents.yaml.example +72 -0
- package/.agents/config/crew-dispatch.json +22 -0
- package/.agents/config/cron.yaml +9 -0
- package/.agents/config/eindri-dispatch.json +67 -0
- package/.agents/config/eindri-harness +1 -0
- package/.agents/config/startup-memory-budget +1 -0
- package/.agents/config/tailscale-sync.yaml +27 -0
- package/.agents/filebrowser/.filebrowser.json +6 -0
- package/.agents/filebrowser/README.md +11 -0
- package/.agents/gateway/README.md +12 -0
- package/.agents/github/README.md +15 -0
- package/.agents/github/webhooks/.gitkeep +0 -0
- package/.agents/github/workflows/.gitkeep +0 -0
- package/.agents/harness/opencode/plugins/lib/rodd-operational-input.js +37 -0
- package/.agents/harness/opencode/plugins/package.json +4 -0
- package/.agents/harness/opencode/plugins/saga-sessionstart.js +77 -0
- package/.agents/harness/opencode/plugins/syn-cd-check.js +61 -0
- package/.agents/harness/opencode/plugins/syn-pretool-check.js +68 -0
- package/.agents/harness/opencode/plugins/syn-turnend-guard.js +105 -0
- package/.agents/harness/opencode/plugins/syn-watch-arm.js +521 -0
- package/.agents/memory/README.md +46 -0
- package/.agents/memory/kaia.engram +0 -0
- package/.agents/memory/kaia.engram-shm +0 -0
- package/.agents/memory/kaia.engram-wal +0 -0
- package/.agents/memory/runes_audit.md +5 -0
- package/.agents/memory/well/episodes.jsonl +367 -0
- package/.agents/memory/well/workspace.jsonl +27 -0
- package/.agents/migrations/0001-hodd-layout.sh +38 -0
- package/.agents/migrations/0002-a2a-mcp.sh +27 -0
- package/.agents/migrations/0003-private-data-separation.sh +111 -0
- package/.agents/migrations/0004-hoard-and-realms.sh +104 -0
- package/.agents/sandbox/Dockerfile.utgard +19 -0
- package/.agents/sandbox/README.md +14 -0
- package/.agents/sandbox/utgard.config.json +14 -0
- package/.agents/skills/README.md +59 -0
- package/.agents/skills/bragi-marketing/README.md +3 -0
- package/.agents/skills/bragi-marketing/SKILL.md +82 -0
- package/.agents/skills/eindri-homes/README.md +3 -0
- package/.agents/skills/eindri-homes/SKILL.md +291 -0
- package/.agents/skills/eindri-homes/assets/control-plane.md +122 -0
- package/.agents/skills/eindri-homes/assets/dispatch.md +80 -0
- package/.agents/skills/frigg-consent/README.md +3 -0
- package/.agents/skills/frigg-consent/SKILL.md +56 -0
- package/.agents/skills/galdr-ymirsystem/README.md +3 -0
- package/.agents/skills/galdr-ymirsystem/SKILL.md +158 -0
- package/.agents/skills/galdr-ymirsystem/assets/README.md +74 -0
- package/.agents/skills/galdr-ymirsystem/assets/brokk-distro-runtime.md +390 -0
- package/.agents/skills/galdr-ymirsystem/assets/build-method.md +113 -0
- package/.agents/skills/galdr-ymirsystem/assets/build-tool-categories.md +178 -0
- package/.agents/skills/galdr-ymirsystem/assets/eindri-orchestration.md +518 -0
- package/.agents/skills/galdr-ymirsystem/assets/eindri-profiles.md +136 -0
- package/.agents/skills/galdr-ymirsystem/assets/harness-integration/README.md +673 -0
- package/.agents/skills/galdr-ymirsystem/assets/harness-integration/claude-code.md +197 -0
- package/.agents/skills/galdr-ymirsystem/assets/harness-integration/codex.md +191 -0
- package/.agents/skills/galdr-ymirsystem/assets/harness-integration/cursor.md +193 -0
- package/.agents/skills/galdr-ymirsystem/assets/harness-integration/opencode.md +262 -0
- package/.agents/skills/galdr-ymirsystem/assets/harness-integration/pi.md +295 -0
- package/.agents/skills/galdr-ymirsystem/assets/hlidskjalf-ui.md +496 -0
- package/.agents/skills/galdr-ymirsystem/assets/installation.md +571 -0
- package/.agents/skills/galdr-ymirsystem/assets/local-models/HOST-DATA.example.md +72 -0
- package/.agents/skills/galdr-ymirsystem/assets/local-models.md +274 -0
- package/.agents/skills/galdr-ymirsystem/assets/memory-well.md +161 -0
- package/.agents/skills/galdr-ymirsystem/assets/nornir-jobs.md +377 -0
- package/.agents/skills/galdr-ymirsystem/assets/norse-naming.md +325 -0
- package/.agents/skills/galdr-ymirsystem/assets/odrerir-hall.md +129 -0
- package/.agents/skills/galdr-ymirsystem/assets/pi-boot/einherjar-spawn.schema.json +81 -0
- package/.agents/skills/galdr-ymirsystem/assets/pi-boot/herdr-profile.toml +75 -0
- package/.agents/skills/galdr-ymirsystem/assets/pi-boot/pi-profile.yml +96 -0
- package/.agents/skills/galdr-ymirsystem/assets/pi-boot/supervision-tree.yml +163 -0
- package/.agents/skills/galdr-ymirsystem/assets/pi-boot-guide.md +58 -0
- package/.agents/skills/galdr-ymirsystem/assets/porting-upstream-to-norse.md +301 -0
- package/.agents/skills/galdr-ymirsystem/assets/principles.md +385 -0
- package/.agents/skills/galdr-ymirsystem/assets/reference-adoption.md +99 -0
- package/.agents/skills/galdr-ymirsystem/assets/registry.md +379 -0
- package/.agents/skills/galdr-ymirsystem/assets/runtime-compliance.md +463 -0
- package/.agents/skills/galdr-ymirsystem/assets/runtime-components.md +553 -0
- package/.agents/skills/galdr-ymirsystem/assets/smidja.md +175 -0
- package/.agents/skills/galdr-ymirsystem/schemas/toon-schemas.md +190 -0
- package/.agents/skills/galdr-ymirsystem/scripts/README.md +45 -0
- package/.agents/skills/galdr-ymirsystem/scripts/bench-one.sh +248 -0
- package/.agents/skills/galdr-ymirsystem/scripts/compliance-check.sh +360 -0
- package/.agents/skills/galdr-ymirsystem/scripts/gpu-sample.sh +43 -0
- package/.agents/skills/galdr-ymirsystem/scripts/stop-all.sh +34 -0
- package/.agents/skills/galdr-ymirsystem/scripts/toon-check.py +188 -0
- package/.agents/skills/gjallarhorn-relay/README.md +3 -0
- package/.agents/skills/gjallarhorn-relay/SKILL.md +283 -0
- package/.agents/skills/groa-update/SKILL.md +70 -0
- package/.agents/skills/hamr-adapters/README.md +3 -0
- package/.agents/skills/hamr-adapters/SKILL.md +123 -0
- package/.agents/skills/hamr-adapters/references/common/control-and-recovery.md +37 -0
- package/.agents/skills/hamr-adapters/references/common/dispatch.md +32 -0
- package/.agents/skills/hamr-adapters/references/common/model-and-effort.md +42 -0
- package/.agents/skills/hamr-adapters/references/common/primary-hooks.md +40 -0
- package/.agents/skills/hamr-adapters/references/harness/claude.md +55 -0
- package/.agents/skills/hamr-adapters/references/harness/codex.md +43 -0
- package/.agents/skills/hamr-adapters/references/harness/cursor.md +75 -0
- package/.agents/skills/hamr-adapters/references/harness/grok.md +69 -0
- package/.agents/skills/hamr-adapters/references/harness/kimi.md +51 -0
- package/.agents/skills/hamr-adapters/references/harness/muse.md +70 -0
- package/.agents/skills/hamr-adapters/references/harness/opencode.md +42 -0
- package/.agents/skills/hamr-adapters/references/harness/pi.md +56 -0
- package/.agents/skills/herdr-panes/README.md +3 -0
- package/.agents/skills/herdr-panes/SKILL.md +92 -0
- package/.agents/skills/herdr-panes/assets/herdr-backend.md +346 -0
- package/.agents/skills/herdr-panes/assets/tmux-backend.md +115 -0
- package/.agents/skills/hnoss-design/DESIGN.md +19 -0
- package/.agents/skills/hnoss-design/README.md +3 -0
- package/.agents/skills/hnoss-design/SKILL.md +83 -0
- package/.agents/skills/hvild-afk/README.md +3 -0
- package/.agents/skills/hvild-afk/SKILL.md +241 -0
- package/.agents/skills/jord-projects/README.md +3 -0
- package/.agents/skills/jord-projects/SKILL.md +92 -0
- package/.agents/skills/lifecycle/SKILL.md +56 -0
- package/.agents/skills/lifecycle/smoke_test.sh +52 -0
- package/.agents/skills/lifecycle/start.sh +21 -0
- package/.agents/skills/lifecycle/status.sh +35 -0
- package/.agents/skills/lifecycle/stop.sh +16 -0
- package/.agents/skills/muninn-stow/README.md +3 -0
- package/.agents/skills/muninn-stow/SKILL.md +309 -0
- package/.agents/skills/no-mistakes/SKILL.md +379 -0
- package/.agents/skills/nornir-schedule/README.md +3 -0
- package/.agents/skills/nornir-schedule/SKILL.md +25 -0
- package/.agents/skills/nornir-schedule/assets/events.md +137 -0
- package/.agents/skills/nornir-schedule/assets/quota.md +131 -0
- package/.agents/skills/nsr-compliance/README.md +3 -0
- package/.agents/skills/nsr-compliance/SKILL.md +29 -0
- package/.agents/skills/nsr-compliance/assets/nsr/README.md +40 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/RULES/README.md +15 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/RULES/git-workflow.md +18 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/RULES/security.md +22 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/RULES/tech-stack.md +8 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/NSRcompliance/SKILL.md +71 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/NSRcompliance/config/core_four.yaml +16 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/NSRcompliance/gates/check_danger.sh +102 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/NSRcompliance/gates/check_wiring.sh +69 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/NSRcompliance/gates/validate_code.sh +29 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/NSRcompliance/gates/verify_docs.py +66 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/NSRcompliance/generate.sh +109 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/NSRcompliance/harness/envelopes/result_envelope.json +14 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/NSRcompliance/harness/envelopes/task_envelope.json +20 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/NSRcompliance/harness/runner.py +303 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/NSRcompliance/telemetry/logger.py +112 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/NSRcompliance/templates/compliance/README.md +18 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/NSRcompliance/templates/compliance/config/README.md +10 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/NSRcompliance/templates/compliance/gates/README.md +12 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/NSRcompliance/templates/compliance/gates/check_env.sh +23 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/NSRcompliance/templates/compliance/gates/check_paths.sh +20 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/NSRcompliance/templates/compliance/gates/check_platform.sh +23 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/NSRcompliance/templates/compliance/harness/README.md +11 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/NSRcompliance/templates/compliance/harness/envelopes/README.md +10 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/NSRcompliance/templates/compliance/installer/README.md +10 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/NSRcompliance/templates/compliance/installer/install.sh +125 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/NSRcompliance/templates/compliance/telemetry/README.md +11 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/README.md +20 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/features/README.md +36 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/features/SKILL-template.md +14 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/git_ops/README.md +25 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/git_ops/SKILL.md +13 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/git_ops/create_branch.sh +6 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/git_ops/safe_commit.sh +9 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/git_ops/sync_upstream.sh +6 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/lifecycle/README.md +27 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/lifecycle/SKILL.md +15 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/lifecycle/smoke_test.sh +12 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/lifecycle/start.sh +19 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/lifecycle/status.sh +12 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/lifecycle/stop.sh +15 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/compliance/README.md +18 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/compliance/config/README.md +10 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/compliance/config/core_four.yaml +13 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/compliance/gates/README.md +12 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/compliance/gates/check_danger.sh +102 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/compliance/gates/check_env.sh +23 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/compliance/gates/check_paths.sh +20 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/compliance/gates/check_platform.sh +23 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/compliance/gates/check_wiring.sh +69 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/compliance/harness/README.md +11 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/compliance/harness/envelopes/README.md +10 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/compliance/harness/envelopes/result_envelope.json +14 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/compliance/harness/envelopes/task_envelope.json +20 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/compliance/harness/runner.py +303 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/compliance/installer/README.md +10 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/compliance/installer/install.sh +125 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/compliance/telemetry/README.md +11 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/compliance/telemetry/logger.py +112 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/BEST_PRACTICES/README.md +14 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/BEST_PRACTICES/api.md +17 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/BEST_PRACTICES/compliance-wiring.md +155 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/BEST_PRACTICES/frontend.md +15 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/CI_CD/README.md +11 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/CI_CD/deployment/README.md +23 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/CI_CD/deployment/clients/README.md +16 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/CI_CD/deployment/clients/template-README.md +11 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/CI_CD/deployment/deploy.sh +49 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/CI_CD/deployment/envs/README.md +12 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/CI_CD/deployment/envs/development.env.example +17 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/CI_CD/deployment/envs/production.env.example +17 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/CI_CD/deployment/envs/staging.env.example +17 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/CI_CD/deployment/tenants/README.md +11 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/CI_CD/deployment/tenants/template.env.example +8 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/DEVELOPER_SETUP/README.md +26 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/DEVELOPER_SETUP/agent.md +7 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/DEVELOPER_SETUP/developers/dev-1.md +20 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/DEVELOPER_SETUP/linux.md +9 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/DEVELOPER_SETUP/macos.md +10 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/DEVELOPER_SETUP/shared.md +19 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/DEVELOPER_SETUP/windows.md +14 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/HOSTING/README.md +25 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/HOSTING/_template.md +41 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/HOSTING/hosting-1.md +41 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/README.md +21 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/RUNBOOK/README.md +15 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/RUNBOOK/deployment.md +19 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/RUNBOOK/disaster-recovery.md +15 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/RUNBOOK/ops-tasks.md +14 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/features/README.md +16 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/project-skills.md +146 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/research/README.md +11 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/research/software-compliance-research.md +269 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/working-with-agents.md +260 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/root/AGENTS.md +38 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/root/ARCHITECTURE.md +36 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/root/BEST_PRACTICES.md +27 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/root/CI_CD.md +23 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/root/FEATURES.md +15 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/root/README.md +22 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/root/RULES.md +37 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/root/STRUCTURE.md +45 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/root/TECH_STACK.md +28 -0
- package/.agents/skills/nsr-compliance/assets/nsr/scaffold-spec.md +63 -0
- package/.agents/skills/nsr-compliance/assets/nsr/scripts/scaffold.sh +103 -0
- package/.agents/skills/nsr-compliance/assets/nsr/scripts/validate.sh +52 -0
- package/.agents/skills/nsr-compliance/assets/nsrcompliance/SKILL.md +71 -0
- package/.agents/skills/nsr-compliance/assets/nsrcompliance/config/core_four.yaml +16 -0
- package/.agents/skills/nsr-compliance/assets/nsrcompliance/gates/check_danger.sh +102 -0
- package/.agents/skills/nsr-compliance/assets/nsrcompliance/gates/check_wiring.sh +69 -0
- package/.agents/skills/nsr-compliance/assets/nsrcompliance/gates/validate_code.sh +29 -0
- package/.agents/skills/nsr-compliance/assets/nsrcompliance/gates/verify_docs.py +66 -0
- package/.agents/skills/nsr-compliance/assets/nsrcompliance/generate.sh +109 -0
- package/.agents/skills/nsr-compliance/assets/nsrcompliance/harness/envelopes/result_envelope.json +14 -0
- package/.agents/skills/nsr-compliance/assets/nsrcompliance/harness/envelopes/task_envelope.json +20 -0
- package/.agents/skills/nsr-compliance/assets/nsrcompliance/harness/runner.py +303 -0
- package/.agents/skills/nsr-compliance/assets/nsrcompliance/telemetry/logger.py +112 -0
- package/.agents/skills/nsr-compliance/assets/nsrcompliance/templates/compliance/README.md +18 -0
- package/.agents/skills/nsr-compliance/assets/nsrcompliance/templates/compliance/config/README.md +10 -0
- package/.agents/skills/nsr-compliance/assets/nsrcompliance/templates/compliance/gates/README.md +12 -0
- package/.agents/skills/nsr-compliance/assets/nsrcompliance/templates/compliance/gates/check_env.sh +23 -0
- package/.agents/skills/nsr-compliance/assets/nsrcompliance/templates/compliance/gates/check_paths.sh +20 -0
- package/.agents/skills/nsr-compliance/assets/nsrcompliance/templates/compliance/gates/check_platform.sh +23 -0
- package/.agents/skills/nsr-compliance/assets/nsrcompliance/templates/compliance/harness/README.md +11 -0
- package/.agents/skills/nsr-compliance/assets/nsrcompliance/templates/compliance/harness/envelopes/README.md +10 -0
- package/.agents/skills/nsr-compliance/assets/nsrcompliance/templates/compliance/installer/README.md +10 -0
- package/.agents/skills/nsr-compliance/assets/nsrcompliance/templates/compliance/installer/install.sh +125 -0
- package/.agents/skills/nsr-compliance/assets/nsrcompliance/templates/compliance/telemetry/README.md +11 -0
- package/.agents/skills/pr-ops/README.md +3 -0
- package/.agents/skills/pr-ops/SKILL.md +109 -0
- package/.agents/skills/ratatoskr-a2a/README.md +3 -0
- package/.agents/skills/ratatoskr-a2a/SKILL.md +148 -0
- package/.agents/skills/rules-check-drift/SKILL.md +81 -0
- package/.agents/skills/saga-bearings/README.md +3 -0
- package/.agents/skills/saga-bearings/SKILL.md +26 -0
- package/.agents/skills/saga-bearings/assets/bearings.md +161 -0
- package/.agents/skills/saga-bearings/assets/board-template.html +731 -0
- package/.agents/skills/saga-bearings/assets/recap.md +56 -0
- package/.agents/skills/syn-recovery/README.md +3 -0
- package/.agents/skills/syn-recovery/SKILL.md +54 -0
- package/.agents/skills/tyr-check/README.md +3 -0
- package/.agents/skills/tyr-check/SKILL.md +218 -0
- package/.agents/skills/urdh-hold/README.md +3 -0
- package/.agents/skills/urdh-hold/SKILL.md +25 -0
- package/.agents/skills/urdh-hold/assets/decisions.md +15 -0
- package/.agents/skills/urdh-hold/assets/hold.md +54 -0
- package/.agents/skills/vor-diagnostics/README.md +3 -0
- package/.agents/skills/vor-diagnostics/SKILL.md +68 -0
- package/.agents/skills/ymir-host/README.md +3 -0
- package/.agents/skills/ymir-host/SKILL.md +34 -0
- package/.agents/skills/ymir-host/assets/desktop.md +36 -0
- package/.agents/skills/ymir-host/assets/omarchy.md +297 -0
- package/.agents/skills/ymir-host/assets/runbooks.md +15 -0
- package/.agents/skills/ymir-host/assets/thjazi.md +220 -0
- package/.agents/skills/ymir-host/assets/update.md +64 -0
- package/.agents/tests/assets/board-render-harness.mjs +117 -0
- package/.agents/tests/cmux-test-safety.sh +51 -0
- package/.agents/tests/fixtures.sh +291 -0
- package/.agents/tests/fm-afk-inject-e2e.test.sh +428 -0
- package/.agents/tests/fm-afk-inject-herdr-e2e.test.sh +536 -0
- package/.agents/tests/fm-afk-launch.test.sh +961 -0
- package/.agents/tests/fm-afk-pi-herdr-return-e2e.test.sh +297 -0
- package/.agents/tests/fm-afk-return.test.sh +283 -0
- package/.agents/tests/fm-arm-pretool-check.test.sh +482 -0
- package/.agents/tests/fm-ask-user-authority.test.sh +42 -0
- package/.agents/tests/fm-backend-autodetect-smoke.test.sh +176 -0
- package/.agents/tests/fm-backend-cmux-smoke.test.sh +188 -0
- package/.agents/tests/fm-backend-cmux.test.sh +1166 -0
- package/.agents/tests/fm-backend-herdr-eventwait-smoke.test.sh +136 -0
- package/.agents/tests/fm-backend-herdr-eventwait.test.py +106 -0
- package/.agents/tests/fm-backend-herdr-focus-flash-e2e.test.sh +409 -0
- package/.agents/tests/fm-backend-herdr-launcher-workspace-e2e.test.sh +437 -0
- package/.agents/tests/fm-backend-herdr-presentation-e2e.test.sh +1424 -0
- package/.agents/tests/fm-backend-herdr-prune-safety-e2e.test.sh +180 -0
- package/.agents/tests/fm-backend-herdr-respawn-idem-e2e.test.sh +182 -0
- package/.agents/tests/fm-backend-herdr-smoke.test.sh +355 -0
- package/.agents/tests/fm-backend-herdr-workspace-per-home-e2e.test.sh +251 -0
- package/.agents/tests/fm-backend-herdr.test.sh +4606 -0
- package/.agents/tests/fm-backend-orca.test.sh +1357 -0
- package/.agents/tests/fm-backend-tmux-smoke.test.sh +173 -0
- package/.agents/tests/fm-backend-zellij-smoke.test.sh +205 -0
- package/.agents/tests/fm-backend-zellij.test.sh +1360 -0
- package/.agents/tests/fm-backend.test.sh +1142 -0
- package/.agents/tests/fm-backlog-atomicity.test.sh +2302 -0
- package/.agents/tests/fm-backlog-handoff.test.sh +1360 -0
- package/.agents/tests/fm-bearings-board-render.test.sh +135 -0
- package/.agents/tests/fm-bearings-board.test.sh +414 -0
- package/.agents/tests/fm-bearings-snapshot.test.sh +1990 -0
- package/.agents/tests/fm-bootstrap-network-parallel.test.sh +316 -0
- package/.agents/tests/fm-bootstrap.test.sh +1178 -0
- package/.agents/tests/fm-branch-supervision.test.sh +552 -0
- package/.agents/tests/fm-brief.test.sh +785 -0
- package/.agents/tests/fm-busy-adapter-wiring.test.sh +324 -0
- package/.agents/tests/fm-busy-state.test.sh +465 -0
- package/.agents/tests/fm-calm-pi-extension.test.sh +3979 -0
- package/.agents/tests/fm-captain-hold-lifecycle.test.sh +1187 -0
- package/.agents/tests/fm-cd-pretool-check.test.sh +400 -0
- package/.agents/tests/fm-check-unregister.test.sh +198 -0
- package/.agents/tests/fm-classify-corr-token.test.sh +543 -0
- package/.agents/tests/fm-classify-decision-key.test.sh +340 -0
- package/.agents/tests/fm-claude-stop-autoarm-live-e2e.test.sh +164 -0
- package/.agents/tests/fm-claude-stop-autoarm.test.sh +1190 -0
- package/.agents/tests/fm-cmux-claude-composer-live-e2e.test.sh +110 -0
- package/.agents/tests/fm-codex-continuity-live-e2e.test.sh +55 -0
- package/.agents/tests/fm-composer-ghost.test.sh +716 -0
- package/.agents/tests/fm-composer-lib.test.sh +666 -0
- package/.agents/tests/fm-composer-matrix-live-e2e.test.sh +220 -0
- package/.agents/tests/fm-control-herdr-smoke.test.sh +149 -0
- package/.agents/tests/fm-control-relaunch.test.sh +1530 -0
- package/.agents/tests/fm-control.test.sh +910 -0
- package/.agents/tests/fm-crew-state.test.sh +1610 -0
- package/.agents/tests/fm-cursor-harness.test.sh +404 -0
- package/.agents/tests/fm-cursor-primary-live-e2e.test.sh +217 -0
- package/.agents/tests/fm-cursor-primary.test.sh +708 -0
- package/.agents/tests/fm-daemon.test.sh +2740 -0
- package/.agents/tests/fm-documentation-audiences.test.sh +141 -0
- package/.agents/tests/fm-ensure-agents-md.test.sh +371 -0
- package/.agents/tests/fm-extension-binding.test.sh +2187 -0
- package/.agents/tests/fm-fleet-snapshot-view.test.sh +816 -0
- package/.agents/tests/fm-fleet-sync.test.sh +721 -0
- package/.agents/tests/fm-gate-refuse.test.sh +356 -0
- package/.agents/tests/fm-gitignore-config.test.sh +89 -0
- package/.agents/tests/fm-gotmp.test.sh +230 -0
- package/.agents/tests/fm-grok-continuity-live-e2e.test.sh +112 -0
- package/.agents/tests/fm-grok-harness.test.sh +117 -0
- package/.agents/tests/fm-grok-stop-live-e2e.test.sh +219 -0
- package/.agents/tests/fm-guard-stale-banner.test.sh +709 -0
- package/.agents/tests/fm-harness-adapter-instructions-live-e2e.test.sh +134 -0
- package/.agents/tests/fm-harness-adapter-references.test.sh +30 -0
- package/.agents/tests/fm-harness-liveness-drift-live-e2e.test.sh +151 -0
- package/.agents/tests/fm-herdr-lab.test.sh +243 -0
- package/.agents/tests/fm-herdr-session-cleanup-e2e.test.sh +146 -0
- package/.agents/tests/fm-herdr-session-cleanup.test.sh +329 -0
- package/.agents/tests/fm-herdr-submit-confirm-live-e2e.test.sh +125 -0
- package/.agents/tests/fm-herdr-version-floor-live-e2e.test.sh +144 -0
- package/.agents/tests/fm-home-summary-refresh.test.sh +971 -0
- package/.agents/tests/fm-inactive-reconcile.test.sh +485 -0
- package/.agents/tests/fm-kimi-harness.test.sh +687 -0
- package/.agents/tests/fm-lint-workflows.test.sh +532 -0
- package/.agents/tests/fm-lint.test.sh +1023 -0
- package/.agents/tests/fm-muse-harness.test.sh +932 -0
- package/.agents/tests/fm-muse-signals-live-e2e.test.sh +205 -0
- package/.agents/tests/fm-no-mistakes-required.test.sh +72 -0
- package/.agents/tests/fm-on.test.sh +507 -0
- package/.agents/tests/fm-opencode-primary-live-e2e.test.sh +357 -0
- package/.agents/tests/fm-operational-input.test.sh +160 -0
- package/.agents/tests/fm-peek-remote.test.sh +110 -0
- package/.agents/tests/fm-pending-reply.test.sh +1288 -0
- package/.agents/tests/fm-pi-branch-extension.test.sh +3182 -0
- package/.agents/tests/fm-pi-branch-live-e2e.test.sh +527 -0
- package/.agents/tests/fm-pi-primary-live-e2e.test.sh +345 -0
- package/.agents/tests/fm-pi-primary-types.test.sh +68 -0
- package/.agents/tests/fm-pi-watch-extension.test.sh +2843 -0
- package/.agents/tests/fm-pr-check-security.test.sh +2140 -0
- package/.agents/tests/fm-pr-merge.test.sh +2135 -0
- package/.agents/tests/fm-procevent-quota.test.sh +225 -0
- package/.agents/tests/fm-procevent-when.test.sh +406 -0
- package/.agents/tests/fm-procevent.test.sh +1745 -0
- package/.agents/tests/fm-project-origin.test.sh +110 -0
- package/.agents/tests/fm-public-followup.test.sh +2272 -0
- package/.agents/tests/fm-quota-array-dispatch-live-e2e.test.sh +526 -0
- package/.agents/tests/fm-quota-choose.test.sh +626 -0
- package/.agents/tests/fm-remote-backlog-handoff.test.sh +453 -0
- package/.agents/tests/fm-remote-doctor.test.sh +625 -0
- package/.agents/tests/fm-remote-entrypoint.test.sh +59 -0
- package/.agents/tests/fm-remote-job-orphan-reap.test.sh +205 -0
- package/.agents/tests/fm-remote-job.test.sh +768 -0
- package/.agents/tests/fm-remote-reply.test.sh +520 -0
- package/.agents/tests/fm-remote-secondmate-lifecycle-e2e.test.sh +1256 -0
- package/.agents/tests/fm-remote-secondmate-parent-binding.test.sh +326 -0
- package/.agents/tests/fm-remote-secondmate-trace-context.test.sh +310 -0
- package/.agents/tests/fm-remote-transport-lanes.test.sh +431 -0
- package/.agents/tests/fm-review-diff.test.sh +176 -0
- package/.agents/tests/fm-secondmate-harness.test.sh +2600 -0
- package/.agents/tests/fm-secondmate-lifecycle-e2e.test.sh +259 -0
- package/.agents/tests/fm-secondmate-liveness.test.sh +559 -0
- package/.agents/tests/fm-secondmate-reconcile.test.sh +732 -0
- package/.agents/tests/fm-secondmate-safety.test.sh +2972 -0
- package/.agents/tests/fm-secondmate-sync.test.sh +913 -0
- package/.agents/tests/fm-send-inbox-doorbell-live-e2e.test.sh +206 -0
- package/.agents/tests/fm-send-inbox.test.sh +352 -0
- package/.agents/tests/fm-send-popup-settle.test.sh +167 -0
- package/.agents/tests/fm-send-remote-delivery.test.sh +796 -0
- package/.agents/tests/fm-send-resolve-key.test.sh +551 -0
- package/.agents/tests/fm-send-secondmate-marker-herdr-e2e.test.sh +187 -0
- package/.agents/tests/fm-send-secondmate-marker.test.sh +278 -0
- package/.agents/tests/fm-send-settle.test.sh +147 -0
- package/.agents/tests/fm-send-strict.test.sh +241 -0
- package/.agents/tests/fm-session-lock-ancestry.test.sh +365 -0
- package/.agents/tests/fm-session-start.test.sh +2515 -0
- package/.agents/tests/fm-sessionstart-hook-live-e2e.test.sh +631 -0
- package/.agents/tests/fm-sessionstart-instruction-refresh-live-e2e.test.sh +230 -0
- package/.agents/tests/fm-sessionstart-nudge.test.sh +1039 -0
- package/.agents/tests/fm-shared-captain-inheritance.test.sh +404 -0
- package/.agents/tests/fm-spawn-batch.test.sh +148 -0
- package/.agents/tests/fm-spawn-dispatch-profile.test.sh +827 -0
- package/.agents/tests/fm-spawn-pool-base-freshen.test.sh +440 -0
- package/.agents/tests/fm-spawn-worktree-settle.test.sh +147 -0
- package/.agents/tests/fm-startup-memory-budget.test.sh +333 -0
- package/.agents/tests/fm-startup-network.test.sh +712 -0
- package/.agents/tests/fm-stow-cascade.test.sh +370 -0
- package/.agents/tests/fm-subagent-pretool-check.test.sh +291 -0
- package/.agents/tests/fm-supervision-events.test.sh +156 -0
- package/.agents/tests/fm-supervision-instructions.test.sh +189 -0
- package/.agents/tests/fm-tangle-guard.test.sh +277 -0
- package/.agents/tests/fm-task-delivery.test.sh +420 -0
- package/.agents/tests/fm-task-inbox.test.sh +513 -0
- package/.agents/tests/fm-teardown-endpoint-safety.test.sh +374 -0
- package/.agents/tests/fm-teardown.test.sh +2665 -0
- package/.agents/tests/fm-test-fixture-cleanup.test.sh +172 -0
- package/.agents/tests/fm-test-fixtures.test.sh +132 -0
- package/.agents/tests/fm-test-isolation-proof.test.sh +278 -0
- package/.agents/tests/fm-test-run.test.sh +1196 -0
- package/.agents/tests/fm-tmux-agent-liveness.test.sh +354 -0
- package/.agents/tests/fm-tmux-submit-busy.test.sh +340 -0
- package/.agents/tests/fm-tool-update-check.test.sh +1044 -0
- package/.agents/tests/fm-trace-context-lib.test.sh +253 -0
- package/.agents/tests/fm-trace-context-spawn.test.sh +581 -0
- package/.agents/tests/fm-transition-lib.test.sh +54 -0
- package/.agents/tests/fm-turnend-guard.test.sh +1822 -0
- package/.agents/tests/fm-update.test.sh +304 -0
- package/.agents/tests/fm-vendor-auth-probe.test.sh +395 -0
- package/.agents/tests/fm-voice-relay.test.sh +4741 -0
- package/.agents/tests/fm-wake-daemon-lifecycle-e2e.test.sh +171 -0
- package/.agents/tests/fm-wake-drain-open-decisions-cursor.test.sh +356 -0
- package/.agents/tests/fm-wake-drain-open-decisions.test.sh +226 -0
- package/.agents/tests/fm-wake-drain-unread-status.test.sh +379 -0
- package/.agents/tests/fm-wake-queue.test.sh +1224 -0
- package/.agents/tests/fm-watch-arm.test.sh +821 -0
- package/.agents/tests/fm-watch-checkpoint.test.sh +87 -0
- package/.agents/tests/fm-watch-recovery-loop.test.sh +223 -0
- package/.agents/tests/fm-watch-triage.test.sh +3889 -0
- package/.agents/tests/fm-watcher-lock.test.sh +1133 -0
- package/.agents/tests/fm-x-mode.test.sh +3042 -0
- package/.agents/tests/gleipnir-guard-pretool-check.test.sh +51 -0
- package/.agents/tests/gleipnir-machine-lock.test.sh +130 -0
- package/.agents/tests/herdr-test-safety.sh +42 -0
- package/.agents/tests/lib.sh +315 -0
- package/.agents/tests/remote-herdr-fixture.sh +125 -0
- package/.agents/tests/secondmate-helpers.sh +193 -0
- package/.agents/tests/smoke.test.sh +93 -0
- package/.agents/tests/wake-helpers.sh +338 -0
- package/.agents/tests/zellij-test-safety.sh +60 -0
- package/.agents/tools/README.md +10 -0
- package/.agents/tools/bin/tasks-cli +267 -0
- package/.agents/tools/tasks-cli.ts +216 -0
- package/AGENTS.md +454 -0
- package/CHANGELOG.md +881 -0
- package/LICENSE +1 -1
- package/NOTICE +63 -0
- package/README.md +113 -59
- package/RULES/01-domains.md +51 -0
- package/RULES/02-agents.md +78 -0
- package/RULES/03-houses.md +61 -0
- package/RULES/04-hoard.md +65 -0
- package/RULES/05-platforms.md +67 -0
- package/RULES/06-append-only.md +58 -0
- package/RULES/07-config.md +45 -0
- package/RULES/08-delivery-gate.md +75 -0
- package/RULES/README.md +20 -0
- package/bin/README.md +107 -0
- package/bin/a2a-mcp.sh +98 -0
- package/bin/a2a-serve.py +103 -0
- package/bin/a2a-serve.sh +8 -0
- package/bin/a2a-talk.sh +50 -0
- package/bin/a2abridge-ensure.sh +108 -0
- package/bin/agent-run.sh +43 -0
- package/bin/agents-config.sh +233 -0
- package/bin/apodex-smoke-test.sh +92 -0
- package/bin/bifrost-bridge.sh +112 -0
- package/bin/bifrost-ingress.sh +77 -0
- package/bin/bootstrap-macos.sh +107 -0
- package/bin/bootstrap-windows.ps1 +112 -0
- package/bin/branch-guard.sh +64 -0
- package/bin/brokk +73 -0
- package/bin/brokk-classify-lib.sh +1770 -0
- package/bin/brokk-lease-lib.sh +222 -0
- package/bin/brokk-lease.sh +193 -0
- package/bin/brokk-lint.sh +65 -0
- package/bin/brokk-send.sh +38 -0
- package/bin/brokk-update.sh +9 -0
- package/bin/brokk-wake-grant.sh +127 -0
- package/bin/brokk-wake-lib.sh +1849 -0
- package/bin/changelog-guard.sh +165 -0
- package/bin/crash-sense.sh +160 -0
- package/bin/desktop-place.sh +224 -0
- package/bin/docs-guard.sh +39 -0
- package/bin/editor-place.sh +207 -0
- package/bin/eindri-acclaim.sh +34 -0
- package/bin/eindri-control.sh +28 -0
- package/bin/eindri-role.sh +83 -0
- package/bin/eindri-seen.sh +41 -0
- package/bin/eindri-send.sh +15 -0
- package/bin/eindri-start.sh +85 -0
- package/bin/eindri-watch.sh +57 -0
- package/bin/einherjar-spawn.sh +576 -0
- package/bin/eir-doctor.sh +112 -0
- package/bin/erindi-brief.sh +271 -0
- package/bin/fleet-apply.sh +68 -0
- package/bin/github-deploy.sh +81 -0
- package/bin/gjallarhorn-expose.sh +102 -0
- package/bin/gjallarhorn-notify.sh +58 -0
- package/bin/gjallarhorn-purge.sh +54 -0
- package/bin/gjallarhorn-tunnel.sh +72 -0
- package/bin/gleipnir-lock-lib.sh +284 -0
- package/bin/groa-update.sh +122 -0
- package/bin/hall-snapshot.sh +92 -0
- package/bin/hamr-harness.sh +246 -0
- package/bin/heimdall-ssh-keys.sh +374 -0
- package/bin/herdr-agents.py +44 -0
- package/bin/herdr-ensure.sh +104 -0
- package/bin/herdr-run.sh +431 -0
- package/bin/hermes-ensure.sh +88 -0
- package/bin/hoard-lib.sh +25 -0
- package/bin/hodd.sh +78 -0
- package/bin/host-sense.sh +199 -0
- package/bin/huginn-research-worker.sh +108 -0
- package/bin/local-model-lock.sh +51 -0
- package/bin/mimir-bridge.py +226 -0
- package/bin/mimir-bridge.sh +87 -0
- package/bin/mimir-ingest.sh +131 -0
- package/bin/mimir.sh +113 -0
- package/bin/mjollnir-webhook.sh +59 -0
- package/bin/mjollnir.sh +85 -0
- package/bin/model-hardware.sh +82 -0
- package/bin/model-resolve.sh +147 -0
- package/bin/models-detect.sh +178 -0
- package/bin/nornir-cron-start.sh +162 -0
- package/bin/nornir-job-daily-briefing.sh +191 -0
- package/bin/nornir-job-git-sync.sh +152 -0
- package/bin/nornir-job-memory-housekeeping.sh +114 -0
- package/bin/nornir-job-observer.sh +166 -0
- package/bin/nornir-job-skillopt-sleep.sh +76 -0
- package/bin/omarchy-hook-install.sh +60 -0
- package/bin/omarchy-install.sh +148 -0
- package/bin/omarchy-plugins.sh +174 -0
- package/bin/omarchy-sense.sh +149 -0
- package/bin/perm-guard.sh +30 -0
- package/bin/pi-agent.sh +68 -0
- package/bin/pi-ensure.sh +77 -0
- package/bin/pi-local.sh +37 -0
- package/bin/pi-seat.sh +63 -0
- package/bin/prereq-ensure.sh +166 -0
- package/bin/private-guard.sh +38 -0
- package/bin/project-git.sh +55 -0
- package/bin/public-guard.sh +70 -0
- package/bin/ratatoskr.sh +49 -0
- package/bin/realm-lib.sh +32 -0
- package/bin/repo-scrub.sh +62 -0
- package/bin/rodd-operational-input.sh +250 -0
- package/bin/runes-append.sh +198 -0
- package/bin/saga-session-start.sh +172 -0
- package/bin/saga-sessionstart-run.sh +79 -0
- package/bin/saga-wake-drain.sh +41 -0
- package/bin/secret-guard.sh +63 -0
- package/bin/sessrumnir-ensure.sh +100 -0
- package/bin/sessrumnir-sync.sh +237 -0
- package/bin/sessrumnir.sh +91 -0
- package/bin/skillopt-setup.sh +76 -0
- package/bin/skuld-branch-outcome.sh +203 -0
- package/bin/skuld-branch-prompt.sh +101 -0
- package/bin/smidja-bootstrap.sh +90 -0
- package/bin/smidja-observe.sh +86 -0
- package/bin/syn-arm-pretool-check.sh +57 -0
- package/bin/syn-asset-pretool-check.sh +76 -0
- package/bin/syn-cd-pretool-check.sh +28 -0
- package/bin/syn-guard-pretool-check.sh +83 -0
- package/bin/syn-turnend-guard.sh +36 -0
- package/bin/syn-watch-arm.sh +99 -0
- package/bin/tailscale-sync.sh +190 -0
- package/bin/telegram-bot.sh +69 -0
- package/bin/toolchain.sh +74 -0
- package/bin/utgard.sh +111 -0
- package/bin/valhalla.sh +115 -0
- package/bin/valknut-load.sh +260 -0
- package/bin/vor-crew-state.sh +124 -0
- package/bin/wedge-notify.sh +47 -0
- package/bin/workspace-provision.sh +80 -0
- package/bin/workspace-rag.sh +150 -0
- package/bin/wyrd-db.sh +47 -0
- package/bin/yggdrasil.sh +152 -0
- package/bin/ymir-edit.sh +145 -0
- package/bin/ymir-install.sh +641 -0
- package/bin/ymir-invite.sh +93 -0
- package/bin/ymir-isolation.sh +88 -0
- package/bin/ymir-migrate.sh +61 -0
- package/bin/ymir-platform.sh +348 -0
- package/bin/ymir-say.sh +97 -0
- package/bin/ymir-setup-auth.sh +108 -0
- package/bin/ymir-validate.sh +173 -0
- package/bin/ymir.js +74 -16
- package/docs/Architecture.md +339 -0
- package/docs/design.md +351 -0
- package/docs/installations/README.md +63 -0
- package/docs/installations/ymir-home.md +93 -0
- package/docs/integration.md +105 -0
- package/docs/lore.md +1056 -0
- package/docs/research/agent-harnesses.md +290 -0
- package/docs/runbooks/README.md +19 -0
- package/docs/runbooks/a2a.md +91 -0
- package/docs/runbooks/agent-permissions.md +99 -0
- package/docs/runbooks/agents.md +57 -0
- package/docs/runbooks/models.md +69 -0
- package/docs/runbooks/realm-onboarding.md +60 -0
- package/docs/runbooks/secrets-and-hoard.md +60 -0
- package/docs/runbooks/updates-and-migrations.md +110 -0
- package/docs/runbooks/worktrees-isolation.md +72 -0
- package/docs/session-start.md +105 -0
- package/docs/skillopt-integration.md +321 -0
- package/docs/sync-system.md +66 -0
- package/docs/workspaces.md +104 -0
- package/docs/ymir-rut.md +260 -0
- package/package.json +44 -14
- package/packaging/build.sh +74 -0
- package/packaging/macos/Ymir Installer.command +43 -0
- package/packaging/windows/ymir-setup.nsi +74 -0
- package/scripts/README.md +22 -0
- package/scripts/electron.sh +210 -0
- package/scripts/start.sh +167 -0
- package/scripts/stop.sh +113 -0
- package/install.sh +0 -35
|
@@ -0,0 +1,1241 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# Shared remote fm-on job-worker protocol.
|
|
3
|
+
#
|
|
4
|
+
# Source this file from the fixed SSH entrypoint, the long-lived worker, or the
|
|
5
|
+
# remote doctor. It owns the per-account queue at
|
|
6
|
+
# ~/.firstmate/remote-job (override only with FM_REMOTE_JOB_STATE_ROOT for
|
|
7
|
+
# isolated tests), the bounded job record, worker installation, and the remote
|
|
8
|
+
# runtime PATH.
|
|
9
|
+
#
|
|
10
|
+
# A published job directory is mode 0700 and contains root, home, argv
|
|
11
|
+
# (NUL-delimited), stdin, seq, stdout, stderr, queue_deadline, timeout, and
|
|
12
|
+
# state; deadline and exit are added as execution advances, cancel is an
|
|
13
|
+
# optional caller-cancellation marker, and .claim may hold owner, owner_start,
|
|
14
|
+
# supervisor, supervisor_start, group, group_start, and armed records while
|
|
15
|
+
# work executes.
|
|
16
|
+
# Stage writes state=queued last. seq is a queue-wide monotonic staging
|
|
17
|
+
# sequence reserved atomically by its persistent .seq-claims directory; the
|
|
18
|
+
# counter is only a forward-moving allocation hint. If the bounded hint walk
|
|
19
|
+
# is exhausted, allocation rescans the claims for the maximum and continues
|
|
20
|
+
# above it. Expired claims are reaped by an independently hourly-rate-limited
|
|
21
|
+
# sweep. seq is the worker's FIFO ordering key within a home, with the job id
|
|
22
|
+
# as the deterministic tiebreak.
|
|
23
|
+
# FIFO is defined over completed stagings: a stage that returns before another
|
|
24
|
+
# begins executes first; concurrently overlapping stagings have no relative
|
|
25
|
+
# ordering contract.
|
|
26
|
+
# The worker atomically claims a job with .claim, establishes its execution
|
|
27
|
+
# deadline, changes state to running, writes bounded stdout/stderr and exit,
|
|
28
|
+
# then publishes state=done last. Callers wait for done, relay stdout and
|
|
29
|
+
# stderr separately, then reap only their completed record. Input, argv,
|
|
30
|
+
# stdout, and stderr are each capped at 1048576 bytes.
|
|
31
|
+
#
|
|
32
|
+
# The worker serves one lane per staged home: jobs for the same home run
|
|
33
|
+
# strictly FIFO in seq order while lanes for different homes run concurrently,
|
|
34
|
+
# so one home's long job never delays another home's commands. Within a lane a
|
|
35
|
+
# deliberately long-blocking poll would still serialize that home's short
|
|
36
|
+
# interactive commands behind its wait window.
|
|
37
|
+
# fm_remote_job_command_preemptible names the read-only long-poll class
|
|
38
|
+
# (fm-remote-delta-read.sh, the reply-log delta read). The worker preempts a
|
|
39
|
+
# running preemptible job as soon as a non-preemptible job is queued for the
|
|
40
|
+
# same home and publishes exit 76 with emptied stdout and stderr, distinct from
|
|
41
|
+
# the poll's exit 75 elapsed-window-with-no-data result. The delta read is
|
|
42
|
+
# non-destructive and cursor-anchored, so the caller's normal re-arm re-reads
|
|
43
|
+
# the same data and a preempted poll loses nothing.
|
|
44
|
+
#
|
|
45
|
+
# A caller that disconnects before its job completes cancels it instead of
|
|
46
|
+
# abandoning it: fm_remote_job_cancel writes a cancel marker into the record,
|
|
47
|
+
# the worker skips a cancelled queued job and terminates a running cancelled
|
|
48
|
+
# job's process group, and whichever side observes terminal publication reaps
|
|
49
|
+
# the finalized record because no result consumer remains. fm_remote_job_wait
|
|
50
|
+
# honors an optional FM_REMOTE_JOB_DISCONNECT_PROBE function name. When set,
|
|
51
|
+
# the probe runs about once per second; a failure cancels the job and fails
|
|
52
|
+
# the wait. The staging entrypoint arms it with a parent-liveness probe so an
|
|
53
|
+
# ssh channel
|
|
54
|
+
# that dies without delivering a signal still cancels the abandoned job.
|
|
55
|
+
# Abandoned .stage.* staging litter older than
|
|
56
|
+
# FM_REMOTE_JOB_STAGE_REAP_SECONDS is reaped by the worker's stale sweep.
|
|
57
|
+
#
|
|
58
|
+
# The worker accepts only a tracked, non-symlink executable named fm-*.sh below
|
|
59
|
+
# its configured FM_ROOT/bin. Every child receives env -i with the composed
|
|
60
|
+
# PATH, HOME, FM_HOME, FM_ROOT_OVERRIDE, and FM_REMOTE_JOB_ACTIVE=1. The PATH
|
|
61
|
+
# is intentionally filesystem-discovered rather than login-shell-derived:
|
|
62
|
+
# ~/.local/bin; nvm, asdf, and mise shims/install bins; Nix; Homebrew; and the
|
|
63
|
+
# system tail. No shell startup files are evaluated. Each discovered set is
|
|
64
|
+
# appended in the shell's own sorted pathname-expansion order, so which install
|
|
65
|
+
# of a multi-version tool wins is fixed by this composition rather than by the
|
|
66
|
+
# order the filesystem happens to return.
|
|
67
|
+
#
|
|
68
|
+
# On macOS the worker is Firstmate's Aqua LaunchAgent
|
|
69
|
+
# dev.firstmate.remote-job at ~/Library/LaunchAgents/dev.firstmate.remote-job.plist
|
|
70
|
+
# with logs under ~/Library/Logs. Linux starts the same worker process without
|
|
71
|
+
# an Aqua requirement. The launch-agent renderer and repair helpers here are
|
|
72
|
+
# shared by the entrypoint and remote doctor so their ownership cannot drift.
|
|
73
|
+
#
|
|
74
|
+
# The Linux start path puts the worker tree in its own process group, so
|
|
75
|
+
# stopping a worker signals its restart supervisor, its serving child, and any
|
|
76
|
+
# job descendant together instead of leaving a supervisor to restart what was
|
|
77
|
+
# just killed. fm_remote_job_stop_worker_tree owns that stop and refuses to
|
|
78
|
+
# signal a group whose leader is not itself a worker, so a worker inherited
|
|
79
|
+
# from an older build or from launchd's own session is still stopped safely as
|
|
80
|
+
# a single process. fm_remote_job_root_is_live is the shared predicate for
|
|
81
|
+
# whether a worker's code root still exists; bin/fm-remote-job-worker.sh uses
|
|
82
|
+
# it to stop itself once its root is pruned, and
|
|
83
|
+
# bin/fm-remote-job-reap-orphans.sh uses it to reap workers that were already
|
|
84
|
+
# orphaned that way.
|
|
85
|
+
|
|
86
|
+
FM_REMOTE_JOB_LABEL=dev.firstmate.remote-job
|
|
87
|
+
FM_REMOTE_JOB_MAX_BYTES=${FM_REMOTE_JOB_MAX_BYTES:-1048576}
|
|
88
|
+
FM_REMOTE_JOB_QUEUE_TIMEOUT=${FM_REMOTE_JOB_QUEUE_TIMEOUT:-360}
|
|
89
|
+
FM_REMOTE_JOB_TIMEOUT=${FM_REMOTE_JOB_TIMEOUT:-360}
|
|
90
|
+
FM_REMOTE_JOB_WAIT_GRACE=${FM_REMOTE_JOB_WAIT_GRACE:-30}
|
|
91
|
+
FM_REMOTE_JOB_POLL_SECONDS=${FM_REMOTE_JOB_POLL_SECONDS:-0.05}
|
|
92
|
+
FM_REMOTE_JOB_REAP_SECONDS=${FM_REMOTE_JOB_REAP_SECONDS:-3600}
|
|
93
|
+
FM_REMOTE_JOB_STAGE_REAP_SECONDS=${FM_REMOTE_JOB_STAGE_REAP_SECONDS:-600}
|
|
94
|
+
FM_REMOTE_JOB_SEQ_CLAIM_REAP_SECONDS=86400
|
|
95
|
+
FM_REMOTE_JOB_SEQ_CLAIM_REAP_INTERVAL=3600
|
|
96
|
+
# shellcheck disable=SC2034 # Shared protocol constant consumed by the worker and sourcing callers.
|
|
97
|
+
FM_REMOTE_JOB_PREEMPTED_EXIT=76
|
|
98
|
+
FM_REMOTE_JOB_OPERATOR_PATH=
|
|
99
|
+
FM_REMOTE_JOB_CHILD_PATH=
|
|
100
|
+
FM_REMOTE_JOB_STATE=
|
|
101
|
+
FM_REMOTE_JOB_JOBS=
|
|
102
|
+
FM_REMOTE_JOB_SEQ_CLAIMS=
|
|
103
|
+
FM_REMOTE_JOB_ID=
|
|
104
|
+
FM_REMOTE_JOB_STDOUT=
|
|
105
|
+
FM_REMOTE_JOB_STDERR=
|
|
106
|
+
FM_REMOTE_JOB_EXIT=
|
|
107
|
+
FM_REMOTE_JOB_ERROR=
|
|
108
|
+
FM_REMOTE_JOB_REPAIRED=0
|
|
109
|
+
|
|
110
|
+
fm_remote_job_die() {
|
|
111
|
+
printf 'error: %s\n' "$1" >&2
|
|
112
|
+
return 1
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
fm_remote_job_safe_id() {
|
|
116
|
+
case "$1" in ''|*[!A-Za-z0-9._-]*) return 1 ;; esac
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
fm_remote_job_command_preemptible() { # <staged argv command>
|
|
120
|
+
case "${1:-}" in fm-remote-delta-read.sh) return 0 ;; *) return 1 ;; esac
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
fm_remote_job_validate_settings() {
|
|
124
|
+
case "$FM_REMOTE_JOB_MAX_BYTES" in ''|*[!0-9]*|0) return 1 ;; esac
|
|
125
|
+
[ "$FM_REMOTE_JOB_MAX_BYTES" -le 1048576 ] || return 1
|
|
126
|
+
case "$FM_REMOTE_JOB_QUEUE_TIMEOUT" in ''|*[!0-9]*|0) return 1 ;; esac
|
|
127
|
+
[ "$FM_REMOTE_JOB_QUEUE_TIMEOUT" -le 3600 ] || return 1
|
|
128
|
+
case "$FM_REMOTE_JOB_TIMEOUT" in ''|*[!0-9]*|0) return 1 ;; esac
|
|
129
|
+
[ "$FM_REMOTE_JOB_TIMEOUT" -le 3600 ] || return 1
|
|
130
|
+
case "$FM_REMOTE_JOB_WAIT_GRACE" in ''|*[!0-9]*) return 1 ;; esac
|
|
131
|
+
[ "$FM_REMOTE_JOB_WAIT_GRACE" -le 300 ] || return 1
|
|
132
|
+
case "$FM_REMOTE_JOB_REAP_SECONDS" in ''|*[!0-9]*|0) return 1 ;; esac
|
|
133
|
+
case "$FM_REMOTE_JOB_STAGE_REAP_SECONDS" in ''|*[!0-9]*|0) return 1 ;; esac
|
|
134
|
+
return 0
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
fm_remote_job_platform() {
|
|
138
|
+
local raw=${FM_REMOTE_JOB_PLATFORM_OVERRIDE:-}
|
|
139
|
+
[ -n "$raw" ] || raw=$(uname -s 2>/dev/null || true)
|
|
140
|
+
case "$raw" in
|
|
141
|
+
Darwin|darwin) printf 'darwin\n' ;;
|
|
142
|
+
Linux|linux) printf 'linux\n' ;;
|
|
143
|
+
'') printf 'unknown\n' ;;
|
|
144
|
+
*) printf '%s\n' "$raw" ;;
|
|
145
|
+
esac
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
fm_remote_job_path_append() { # <directory>
|
|
149
|
+
case ":$FM_REMOTE_JOB_OPERATOR_PATH:" in *":$1:"*) return 0 ;; esac
|
|
150
|
+
FM_REMOTE_JOB_OPERATOR_PATH="${FM_REMOTE_JOB_OPERATOR_PATH:+$FM_REMOTE_JOB_OPERATOR_PATH:}$1"
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
fm_remote_job_path_append_if_dir() { # <directory>
|
|
154
|
+
[ -d "$1" ] && [ ! -L "$1" ] || return 0
|
|
155
|
+
fm_remote_job_path_append "$1"
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
fm_remote_job_path_append_resolved_dir() { # <directory>
|
|
159
|
+
local directory physical
|
|
160
|
+
directory=$1
|
|
161
|
+
[ -d "$directory" ] || return 0
|
|
162
|
+
if [ ! -L "$directory" ]; then
|
|
163
|
+
fm_remote_job_path_append "$directory"
|
|
164
|
+
return 0
|
|
165
|
+
fi
|
|
166
|
+
physical=$(CDPATH='' cd -- "$directory" 2>/dev/null && pwd -P) || return 0
|
|
167
|
+
[ -d "$physical" ] && [ ! -L "$physical" ] || return 0
|
|
168
|
+
fm_remote_job_path_append "$physical"
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
# Callers pass an already-expanded glob rather than the pattern, because only
|
|
172
|
+
# the shell's own pathname expansion sorts its matches: bash sorts
|
|
173
|
+
# glob_filename's result in pathexp.c, while `compgen -G` reaches the same
|
|
174
|
+
# glob_filename through pcomplete.c, which does not sort. On bash 3.2 (macOS
|
|
175
|
+
# /bin/bash) that handed back raw readdir order, so which install of a
|
|
176
|
+
# multi-version tool a remote job resolved depended on the filesystem instead
|
|
177
|
+
# of on this composition.
|
|
178
|
+
fm_remote_job_append_dirs() { # <expanded glob matches>
|
|
179
|
+
local directory
|
|
180
|
+
for directory in "$@"; do
|
|
181
|
+
fm_remote_job_path_append_if_dir "$directory"
|
|
182
|
+
done
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
fm_remote_job_nvm_default_selector() { # <account-home>
|
|
186
|
+
local account_home=$1 alias_root selector alias_file next depth=0 suffix
|
|
187
|
+
alias_root="$account_home/.nvm/alias"
|
|
188
|
+
selector=$(fm_remote_job_read_single_line "$alias_root/default" 256 2>/dev/null || true)
|
|
189
|
+
while [ -n "$selector" ] && [ "$depth" -lt 8 ]; do
|
|
190
|
+
case "$selector" in ''|/*|*..*|*[!A-Za-z0-9._*/-]*) return 1 ;; esac
|
|
191
|
+
case "$selector" in
|
|
192
|
+
lts/*) suffix=${selector#lts/}; case "$suffix" in ''|*/*) return 1 ;; esac ;;
|
|
193
|
+
*/*) return 1 ;;
|
|
194
|
+
esac
|
|
195
|
+
alias_file="$alias_root/$selector"
|
|
196
|
+
if [ -f "$alias_file" ] && [ ! -L "$alias_file" ]; then
|
|
197
|
+
next=$(fm_remote_job_read_single_line "$alias_file" 256 2>/dev/null || true)
|
|
198
|
+
[ -n "$next" ] && [ "$next" != "$selector" ] || return 1
|
|
199
|
+
selector=$next
|
|
200
|
+
depth=$((depth + 1))
|
|
201
|
+
continue
|
|
202
|
+
fi
|
|
203
|
+
printf '%s\n' "$selector"
|
|
204
|
+
return 0
|
|
205
|
+
done
|
|
206
|
+
return 1
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
fm_remote_job_nvm_selected_bin() { # <account-home>
|
|
210
|
+
local account_home=$1 selector normalized directory base version major minor patch extra
|
|
211
|
+
local selected='' fallback='' selected_major=-1 selected_minor=-1 selected_patch=-1
|
|
212
|
+
local fallback_major=-1 fallback_minor=-1 fallback_patch=-1 matches
|
|
213
|
+
selector=$(fm_remote_job_nvm_default_selector "$account_home" 2>/dev/null || true)
|
|
214
|
+
[ "$selector" != system ] || return 0
|
|
215
|
+
case "$selector" in node|stable|unstable) normalized= ;; v*) normalized=${selector#v} ;; *) normalized=$selector ;; esac
|
|
216
|
+
case "$normalized" in *[!0-9.]*|.*|*.|*..*) normalized=invalid ;; esac
|
|
217
|
+
for directory in "$account_home"/.nvm/versions/node/*/bin; do
|
|
218
|
+
[ -d "$directory" ] && [ ! -L "$directory" ] || continue
|
|
219
|
+
base=${directory%/bin}
|
|
220
|
+
version=${base##*/}
|
|
221
|
+
version=${version#v}
|
|
222
|
+
IFS=. read -r major minor patch extra <<< "$version"
|
|
223
|
+
case "$major:$minor:$patch:$extra" in *[!0-9:]*) continue ;; esac
|
|
224
|
+
[ -n "$major" ] && [ -n "$minor" ] && [ -n "$patch" ] && [ -z "$extra" ] || continue
|
|
225
|
+
if [ "$major" -gt "$fallback_major" ] ||
|
|
226
|
+
{ [ "$major" -eq "$fallback_major" ] && [ "$minor" -gt "$fallback_minor" ]; } ||
|
|
227
|
+
{ [ "$major" -eq "$fallback_major" ] && [ "$minor" -eq "$fallback_minor" ] && [ "$patch" -gt "$fallback_patch" ]; }; then
|
|
228
|
+
fallback=$directory
|
|
229
|
+
fallback_major=$major
|
|
230
|
+
fallback_minor=$minor
|
|
231
|
+
fallback_patch=$patch
|
|
232
|
+
fi
|
|
233
|
+
matches=0
|
|
234
|
+
if [ -z "$normalized" ]; then
|
|
235
|
+
matches=1
|
|
236
|
+
elif [ "$normalized" != invalid ]; then
|
|
237
|
+
case ".$version." in
|
|
238
|
+
."$normalized".|."$normalized".*) matches=1 ;;
|
|
239
|
+
esac
|
|
240
|
+
fi
|
|
241
|
+
[ "$matches" -eq 1 ] || continue
|
|
242
|
+
if [ "$major" -gt "$selected_major" ] ||
|
|
243
|
+
{ [ "$major" -eq "$selected_major" ] && [ "$minor" -gt "$selected_minor" ]; } ||
|
|
244
|
+
{ [ "$major" -eq "$selected_major" ] && [ "$minor" -eq "$selected_minor" ] && [ "$patch" -gt "$selected_patch" ]; }; then
|
|
245
|
+
selected=$directory
|
|
246
|
+
selected_major=$major
|
|
247
|
+
selected_minor=$minor
|
|
248
|
+
selected_patch=$patch
|
|
249
|
+
fi
|
|
250
|
+
done
|
|
251
|
+
if [ -n "$selected" ]; then
|
|
252
|
+
printf '%s\n' "$selected"
|
|
253
|
+
elif [ -n "$fallback" ]; then
|
|
254
|
+
printf '%s\n' "$fallback"
|
|
255
|
+
fi
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
fm_remote_job_compose_operator_path() { # <account-home>
|
|
259
|
+
local account_home=$1 account_user nvm_bin
|
|
260
|
+
FM_REMOTE_JOB_OPERATOR_PATH=
|
|
261
|
+
fm_remote_job_path_append_if_dir "$account_home/.local/bin"
|
|
262
|
+
nvm_bin=$(fm_remote_job_nvm_selected_bin "$account_home" 2>/dev/null || true)
|
|
263
|
+
[ -z "$nvm_bin" ] || fm_remote_job_path_append "$nvm_bin"
|
|
264
|
+
fm_remote_job_path_append_if_dir "$account_home/.asdf/shims"
|
|
265
|
+
fm_remote_job_append_dirs "$account_home"/.asdf/installs/*/*/bin
|
|
266
|
+
fm_remote_job_path_append_if_dir "$account_home/.local/share/mise/shims"
|
|
267
|
+
fm_remote_job_path_append_if_dir "$account_home/.mise/shims"
|
|
268
|
+
fm_remote_job_append_dirs "$account_home"/.local/share/mise/installs/*/*/bin
|
|
269
|
+
fm_remote_job_append_dirs "$account_home"/.mise/installs/*/*/bin
|
|
270
|
+
fm_remote_job_path_append_resolved_dir "$account_home/.nix-profile/bin"
|
|
271
|
+
account_user=$(id -un 2>/dev/null || true)
|
|
272
|
+
if [ -n "$account_user" ]; then
|
|
273
|
+
fm_remote_job_path_append_resolved_dir "/etc/profiles/per-user/$account_user/bin"
|
|
274
|
+
fi
|
|
275
|
+
fm_remote_job_path_append_resolved_dir /run/current-system/sw/bin
|
|
276
|
+
fm_remote_job_path_append_if_dir /opt/homebrew/bin
|
|
277
|
+
fm_remote_job_path_append_if_dir /usr/local/bin
|
|
278
|
+
fm_remote_job_path_append /usr/bin
|
|
279
|
+
fm_remote_job_path_append /bin
|
|
280
|
+
fm_remote_job_path_append /usr/sbin
|
|
281
|
+
fm_remote_job_path_append /sbin
|
|
282
|
+
printf '%s\n' "$FM_REMOTE_JOB_OPERATOR_PATH"
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
fm_remote_job_build_child_path() { # <remote-root>
|
|
286
|
+
local root=$1 directory old_ifs
|
|
287
|
+
FM_REMOTE_JOB_CHILD_PATH="$root/bin"
|
|
288
|
+
old_ifs=$IFS
|
|
289
|
+
IFS=:
|
|
290
|
+
for directory in $FM_REMOTE_JOB_OPERATOR_PATH; do
|
|
291
|
+
case ":$FM_REMOTE_JOB_CHILD_PATH:" in *":$directory:"*) continue ;; esac
|
|
292
|
+
FM_REMOTE_JOB_CHILD_PATH="$FM_REMOTE_JOB_CHILD_PATH:$directory"
|
|
293
|
+
done
|
|
294
|
+
IFS=$old_ifs
|
|
295
|
+
printf '%s\n' "$FM_REMOTE_JOB_CHILD_PATH"
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
fm_remote_job_operator_tool() { # <tool>; resolves only outside the checkout bin
|
|
299
|
+
local tool=$1 resolved
|
|
300
|
+
resolved=$(PATH="$FM_REMOTE_JOB_OPERATOR_PATH" command -v "$tool" 2>/dev/null || true)
|
|
301
|
+
case "$resolved" in
|
|
302
|
+
/*)
|
|
303
|
+
[ -x "$resolved" ] || return 1
|
|
304
|
+
case ":$FM_REMOTE_JOB_OPERATOR_PATH:" in *":${resolved%/*}:"*) printf '%s\n' "$resolved" ;; *) return 1 ;; esac
|
|
305
|
+
;;
|
|
306
|
+
*) return 1 ;;
|
|
307
|
+
esac
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
fm_remote_job_manager_tool() { # <account-home> <tool>
|
|
311
|
+
local account_home=$1 tool=$2 directory candidate
|
|
312
|
+
for directory in \
|
|
313
|
+
"$account_home"/.nvm/versions/node/*/bin \
|
|
314
|
+
"$account_home"/.asdf/shims \
|
|
315
|
+
"$account_home"/.asdf/installs/*/*/bin \
|
|
316
|
+
"$account_home"/.local/share/mise/shims \
|
|
317
|
+
"$account_home"/.mise/shims \
|
|
318
|
+
"$account_home"/.local/share/mise/installs/*/*/bin \
|
|
319
|
+
"$account_home"/.mise/installs/*/*/bin; do
|
|
320
|
+
[ -d "$directory" ] && [ ! -L "$directory" ] || continue
|
|
321
|
+
candidate="$directory/$tool"
|
|
322
|
+
[ -f "$candidate" ] && [ ! -L "$candidate" ] && [ -x "$candidate" ] || continue
|
|
323
|
+
printf '%s\n' "$candidate"
|
|
324
|
+
return 0
|
|
325
|
+
done
|
|
326
|
+
return 1
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
fm_remote_job_has_forbidden_text_bytes() { # <file>
|
|
330
|
+
LC_ALL=C tr -cd '\000\012\015' < "$1" | LC_ALL=C wc -c | tr -d ' '
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
fm_remote_job_normalize_absolute_path() { # <absolute-path>
|
|
334
|
+
local path=$1 part old_ifs out=/
|
|
335
|
+
case "$path" in /*) ;; *) return 1 ;; esac
|
|
336
|
+
case "$path" in *'//'*) return 1 ;; esac
|
|
337
|
+
old_ifs=$IFS
|
|
338
|
+
IFS=/
|
|
339
|
+
for part in $path; do
|
|
340
|
+
case "$part" in
|
|
341
|
+
'') ;;
|
|
342
|
+
.|..) IFS=$old_ifs; return 1 ;;
|
|
343
|
+
*)
|
|
344
|
+
case "$part" in *$'\n'*|*$'\r'*|*$'\t'*) IFS=$old_ifs; return 1 ;; esac
|
|
345
|
+
if [ "$out" = / ]; then out="/$part"; else out="$out/$part"; fi
|
|
346
|
+
;;
|
|
347
|
+
esac
|
|
348
|
+
done
|
|
349
|
+
IFS=$old_ifs
|
|
350
|
+
printf '%s\n' "$out"
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
fm_remote_job_canonical_existing_dir() { # <path>
|
|
354
|
+
local path=$1 normalized physical
|
|
355
|
+
normalized=$(fm_remote_job_normalize_absolute_path "$path") || return 1
|
|
356
|
+
[ "$normalized" != / ] || return 1
|
|
357
|
+
[ -d "$normalized" ] && [ ! -L "$normalized" ] || return 1
|
|
358
|
+
physical=$(CDPATH='' cd -- "$normalized" 2>/dev/null && pwd -P) || return 1
|
|
359
|
+
[ "$physical" = "$normalized" ] || return 1
|
|
360
|
+
printf '%s\n' "$physical"
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
fm_remote_job_canonical_home() { # <path>; one absent leaf is allowed
|
|
364
|
+
local path=$1 normalized parent base parent_real
|
|
365
|
+
normalized=$(fm_remote_job_normalize_absolute_path "$path") || return 1
|
|
366
|
+
[ "$normalized" != / ] || return 1
|
|
367
|
+
if [ -e "$normalized" ] || [ -L "$normalized" ]; then
|
|
368
|
+
fm_remote_job_canonical_existing_dir "$normalized"
|
|
369
|
+
return
|
|
370
|
+
fi
|
|
371
|
+
parent=$(dirname "$normalized")
|
|
372
|
+
base=$(basename "$normalized")
|
|
373
|
+
case "$base" in ''|.|..) return 1 ;; esac
|
|
374
|
+
parent_real=$(fm_remote_job_canonical_existing_dir "$parent") || return 1
|
|
375
|
+
[ "$parent_real/$base" = "$normalized" ] || return 1
|
|
376
|
+
printf '%s\n' "$normalized"
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
fm_remote_job_safe_child_dir() { # <canonical-parent> <single child basename>
|
|
380
|
+
local parent=$1 base=$2 candidate physical
|
|
381
|
+
case "$base" in ''|*/*|.|..) return 1 ;; esac
|
|
382
|
+
[ -d "$parent" ] && [ ! -L "$parent" ] || return 1
|
|
383
|
+
candidate="$parent/$base"
|
|
384
|
+
if [ -e "$candidate" ] || [ -L "$candidate" ]; then
|
|
385
|
+
[ -d "$candidate" ] && [ ! -L "$candidate" ] || return 1
|
|
386
|
+
else
|
|
387
|
+
(umask 077; mkdir "$candidate") || return 1
|
|
388
|
+
fi
|
|
389
|
+
chmod 700 "$candidate" 2>/dev/null || return 1
|
|
390
|
+
physical=$(CDPATH='' cd -- "$candidate" 2>/dev/null && pwd -P) || return 1
|
|
391
|
+
[ "$physical" = "$candidate" ] || return 1
|
|
392
|
+
printf '%s\n' "$physical"
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
fm_remote_job_prepare_state() { # <account-home>
|
|
396
|
+
local account_home=$1 root parent base firstmate
|
|
397
|
+
fm_remote_job_validate_settings || {
|
|
398
|
+
FM_REMOTE_JOB_ERROR="remote job bounds or timeout are invalid"
|
|
399
|
+
return 1
|
|
400
|
+
}
|
|
401
|
+
account_home=$(fm_remote_job_canonical_existing_dir "$account_home") || {
|
|
402
|
+
FM_REMOTE_JOB_ERROR="remote account home is unavailable or unsafe"
|
|
403
|
+
return 1
|
|
404
|
+
}
|
|
405
|
+
if [ -n "${FM_REMOTE_JOB_STATE_ROOT:-}" ]; then
|
|
406
|
+
root=$(fm_remote_job_normalize_absolute_path "$FM_REMOTE_JOB_STATE_ROOT") || {
|
|
407
|
+
FM_REMOTE_JOB_ERROR="remote job state root is not a safe absolute path"
|
|
408
|
+
return 1
|
|
409
|
+
}
|
|
410
|
+
parent=$(dirname "$root")
|
|
411
|
+
base=$(basename "$root")
|
|
412
|
+
parent=$(fm_remote_job_canonical_existing_dir "$parent") || {
|
|
413
|
+
FM_REMOTE_JOB_ERROR="remote job state parent is unavailable or unsafe"
|
|
414
|
+
return 1
|
|
415
|
+
}
|
|
416
|
+
[ "$parent/$base" = "$root" ] || return 1
|
|
417
|
+
root=$(fm_remote_job_safe_child_dir "$parent" "$base") || {
|
|
418
|
+
FM_REMOTE_JOB_ERROR="remote job state root is unsafe"
|
|
419
|
+
return 1
|
|
420
|
+
}
|
|
421
|
+
else
|
|
422
|
+
firstmate=$(fm_remote_job_safe_child_dir "$account_home" .firstmate) || {
|
|
423
|
+
FM_REMOTE_JOB_ERROR="cannot prepare $account_home/.firstmate for remote jobs"
|
|
424
|
+
return 1
|
|
425
|
+
}
|
|
426
|
+
root=$(fm_remote_job_safe_child_dir "$firstmate" remote-job) || {
|
|
427
|
+
FM_REMOTE_JOB_ERROR="cannot prepare remote job state"
|
|
428
|
+
return 1
|
|
429
|
+
}
|
|
430
|
+
fi
|
|
431
|
+
FM_REMOTE_JOB_STATE=$root
|
|
432
|
+
FM_REMOTE_JOB_JOBS=$(fm_remote_job_safe_child_dir "$FM_REMOTE_JOB_STATE" jobs) || {
|
|
433
|
+
FM_REMOTE_JOB_ERROR="remote job queue is unsafe"
|
|
434
|
+
return 1
|
|
435
|
+
}
|
|
436
|
+
FM_REMOTE_JOB_SEQ_CLAIMS=$(fm_remote_job_safe_child_dir "$FM_REMOTE_JOB_STATE" .seq-claims) || {
|
|
437
|
+
FM_REMOTE_JOB_ERROR="remote job sequence claims are unsafe"
|
|
438
|
+
return 1
|
|
439
|
+
}
|
|
440
|
+
fm_remote_job_safe_child_dir "$FM_REMOTE_JOB_STATE" logs >/dev/null || {
|
|
441
|
+
FM_REMOTE_JOB_ERROR="remote job log directory is unsafe"
|
|
442
|
+
return 1
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
fm_remote_job_job_dir() { # <id>
|
|
447
|
+
local id=$1 dir physical
|
|
448
|
+
fm_remote_job_safe_id "$id" || return 1
|
|
449
|
+
[ -n "$FM_REMOTE_JOB_JOBS" ] || return 1
|
|
450
|
+
dir="$FM_REMOTE_JOB_JOBS/$id"
|
|
451
|
+
[ -d "$dir" ] && [ ! -L "$dir" ] || return 1
|
|
452
|
+
physical=$(CDPATH='' cd -- "$dir" 2>/dev/null && pwd -P) || return 1
|
|
453
|
+
[ "$physical" = "$dir" ] || return 1
|
|
454
|
+
printf '%s\n' "$physical"
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
fm_remote_job_regular_bounded() { # <file> <max-bytes>
|
|
458
|
+
local file=$1 max=$2 bytes
|
|
459
|
+
[ -f "$file" ] && [ ! -L "$file" ] || return 1
|
|
460
|
+
bytes=$(LC_ALL=C wc -c < "$file" | tr -d ' ') || return 1
|
|
461
|
+
case "$bytes" in ''|*[!0-9]*) return 1 ;; esac
|
|
462
|
+
[ "$bytes" -le "$max" ]
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
fm_remote_job_remove_claim_records() { # <claim-dir>
|
|
466
|
+
local claim=$1 file
|
|
467
|
+
[ -d "$claim" ] && [ ! -L "$claim" ] || return 1
|
|
468
|
+
for file in "$claim"/owner "$claim"/owner_start "$claim"/supervisor \
|
|
469
|
+
"$claim"/supervisor_start "$claim"/group "$claim"/group_start "$claim"/armed \
|
|
470
|
+
"$claim"/.owner.* "$claim"/.owner_start.* "$claim"/.supervisor.* \
|
|
471
|
+
"$claim"/.supervisor_start.* "$claim"/.group.* "$claim"/.group_start.* \
|
|
472
|
+
"$claim"/.armed.*; do
|
|
473
|
+
[ -e "$file" ] || [ -L "$file" ] || continue
|
|
474
|
+
fm_remote_job_regular_bounded "$file" 256 || return 1
|
|
475
|
+
rm -f -- "$file" || return 1
|
|
476
|
+
done
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
fm_remote_job_write_state() { # <job-dir> queued|running|done
|
|
480
|
+
local job=$1 value=$2 tmp
|
|
481
|
+
case "$value" in queued|running|done) ;; *) return 1 ;; esac
|
|
482
|
+
[ -d "$job" ] && [ ! -L "$job" ] || return 1
|
|
483
|
+
tmp=$(umask 077; mktemp "$job/.state.XXXXXX") || return 1
|
|
484
|
+
printf '%s\n' "$value" > "$tmp" || { rm -f -- "$tmp"; return 1; }
|
|
485
|
+
chmod 600 "$tmp" || { rm -f -- "$tmp"; return 1; }
|
|
486
|
+
mv -f -- "$tmp" "$job/state"
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
fm_remote_job_read_state() { # <job-dir>
|
|
490
|
+
local job=$1 value extra
|
|
491
|
+
fm_remote_job_regular_bounded "$job/state" 64 || return 1
|
|
492
|
+
IFS= read -r value < "$job/state" || return 1
|
|
493
|
+
if IFS= read -r extra < <(tail -n +2 "$job/state"); then
|
|
494
|
+
: "$extra"
|
|
495
|
+
return 1
|
|
496
|
+
fi
|
|
497
|
+
case "$value" in queued|running|'done') printf '%s\n' "$value" ;; *) return 1 ;; esac
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
fm_remote_job_read_number() { # <job-dir> queue_deadline|timeout|deadline|seq
|
|
501
|
+
local job=$1 field=$2 value
|
|
502
|
+
case "$field" in queue_deadline|timeout|deadline|seq) ;; *) return 1 ;; esac
|
|
503
|
+
fm_remote_job_regular_bounded "$job/$field" 32 || return 1
|
|
504
|
+
value=$(tr -d '\n' < "$job/$field")
|
|
505
|
+
case "$value" in ''|*[!0-9]*) return 1 ;; esac
|
|
506
|
+
[ "$value" -gt 0 ] || return 1
|
|
507
|
+
printf '%s\n' "$value"
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
fm_remote_job_write_number() { # <job-dir> queue_deadline|timeout|deadline|seq <value>
|
|
511
|
+
local job=$1 field=$2 value=$3 tmp
|
|
512
|
+
case "$field" in queue_deadline|timeout|deadline|seq) ;; *) return 1 ;; esac
|
|
513
|
+
case "$value" in ''|*[!0-9]*|0) return 1 ;; esac
|
|
514
|
+
[ -d "$job" ] && [ ! -L "$job" ] || return 1
|
|
515
|
+
tmp=$(umask 077; mktemp "$job/.$field.XXXXXX") || return 1
|
|
516
|
+
printf '%s\n' "$value" > "$tmp" || { rm -f -- "$tmp"; return 1; }
|
|
517
|
+
chmod 600 "$tmp" || { rm -f -- "$tmp"; return 1; }
|
|
518
|
+
mv -f -- "$tmp" "$job/$field"
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
fm_remote_job_read_deadline() { # <job-dir>
|
|
522
|
+
fm_remote_job_read_number "$1" deadline
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
fm_remote_job_advance_seq_hint() { # <value>
|
|
526
|
+
local value=$1 counter current tmp
|
|
527
|
+
counter="$FM_REMOTE_JOB_STATE/seq"
|
|
528
|
+
current=$(cat "$counter" 2>/dev/null || true)
|
|
529
|
+
case "$current" in ''|*[!0-9]*) current=0 ;; esac
|
|
530
|
+
[ "$value" -gt "$current" ] || return 0
|
|
531
|
+
tmp=$(umask 077; mktemp "$FM_REMOTE_JOB_STATE/.seqhint.XXXXXX") || return 1
|
|
532
|
+
printf '%s\n' "$value" > "$tmp" || { rm -f -- "$tmp"; return 1; }
|
|
533
|
+
chmod 600 "$tmp" || { rm -f -- "$tmp"; return 1; }
|
|
534
|
+
current=$(cat "$counter" 2>/dev/null || true)
|
|
535
|
+
case "$current" in ''|*[!0-9]*) current=0 ;; esac
|
|
536
|
+
if [ "$value" -gt "$current" ]; then
|
|
537
|
+
mv -f -- "$tmp" "$counter" || { rm -f -- "$tmp"; return 1; }
|
|
538
|
+
else
|
|
539
|
+
rm -f -- "$tmp"
|
|
540
|
+
fi
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
fm_remote_job_next_seq() { # [stage-dir destination]
|
|
544
|
+
local stage=${1:-} destination=${2:-} counter value claim attempt=0 recovered=0 maximum entry
|
|
545
|
+
[ -n "$FM_REMOTE_JOB_STATE" ] && [ -n "$FM_REMOTE_JOB_SEQ_CLAIMS" ] || return 1
|
|
546
|
+
counter="$FM_REMOTE_JOB_STATE/seq"
|
|
547
|
+
value=$(cat "$counter" 2>/dev/null || true)
|
|
548
|
+
case "$value" in ''|*[!0-9]*) value=0 ;; esac
|
|
549
|
+
while :; do
|
|
550
|
+
if [ "$attempt" -ge 100000 ]; then
|
|
551
|
+
[ "$recovered" -eq 0 ] || return 1
|
|
552
|
+
maximum=0
|
|
553
|
+
for entry in "$FM_REMOTE_JOB_SEQ_CLAIMS"/*; do
|
|
554
|
+
[ -d "$entry" ] && [ ! -L "$entry" ] || continue
|
|
555
|
+
entry=${entry##*/}
|
|
556
|
+
case "$entry" in ''|*[!0-9]*|0) continue ;; esac
|
|
557
|
+
[ "$entry" -le "$maximum" ] || maximum=$entry
|
|
558
|
+
done
|
|
559
|
+
value=$maximum
|
|
560
|
+
attempt=0
|
|
561
|
+
recovered=1
|
|
562
|
+
fi
|
|
563
|
+
attempt=$((attempt + 1))
|
|
564
|
+
value=$((value + 1))
|
|
565
|
+
claim="$FM_REMOTE_JOB_SEQ_CLAIMS/$value"
|
|
566
|
+
if (umask 077; mkdir "$claim") 2>/dev/null; then
|
|
567
|
+
chmod 700 "$claim" || return 1
|
|
568
|
+
fm_remote_job_advance_seq_hint "$value" || true
|
|
569
|
+
if [ -n "$stage" ]; then
|
|
570
|
+
if ! fm_remote_job_write_number "$stage" seq "$value" \
|
|
571
|
+
|| ! fm_remote_job_write_state "$stage" queued \
|
|
572
|
+
|| ! mv -- "$stage" "$destination"; then
|
|
573
|
+
rm -f -- "$stage/state" "$stage/seq"
|
|
574
|
+
return 1
|
|
575
|
+
fi
|
|
576
|
+
rm -f -- "$destination/.owner-pid" "$destination/.owner-start" || true
|
|
577
|
+
fi
|
|
578
|
+
printf '%s\n' "$value"
|
|
579
|
+
return 0
|
|
580
|
+
fi
|
|
581
|
+
[ -d "$claim" ] && [ ! -L "$claim" ] || return 1
|
|
582
|
+
done
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
fm_remote_job_cancelled() { # <job-dir>
|
|
586
|
+
[ -f "$1/cancel" ] && [ ! -L "$1/cancel" ]
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
# Mark a job cancelled on behalf of a disconnected or abandoning caller. The
|
|
590
|
+
# marker never rewrites state: the worker observes it, skips a cancelled queued
|
|
591
|
+
# job, and stops a running cancelled job's process group. The worker reaps after
|
|
592
|
+
# terminal publication; if publication already won the race, this function
|
|
593
|
+
# reaps instead. Cancelling a job that disappeared is a harmless no-op.
|
|
594
|
+
fm_remote_job_cancel() { # <account-home> <id>
|
|
595
|
+
local account_home=$1 id=$2 job state tmp
|
|
596
|
+
fm_remote_job_prepare_state "$account_home" || return 1
|
|
597
|
+
job=$(fm_remote_job_job_dir "$id" 2>/dev/null) || return 0
|
|
598
|
+
state=$(fm_remote_job_read_state "$job" 2>/dev/null || true)
|
|
599
|
+
if [ "$state" = 'done' ]; then
|
|
600
|
+
fm_remote_job_reap "$account_home" "$id" 2>/dev/null || true
|
|
601
|
+
return 0
|
|
602
|
+
fi
|
|
603
|
+
tmp=$(umask 077; mktemp "$job/.cancel.XXXXXX") || return 1
|
|
604
|
+
printf 'cancelled: caller disconnected or abandoned the job\n' > "$tmp" || { rm -f -- "$tmp"; return 1; }
|
|
605
|
+
chmod 600 "$tmp" || { rm -f -- "$tmp"; return 1; }
|
|
606
|
+
mv -f -- "$tmp" "$job/cancel" || return 1
|
|
607
|
+
state=$(fm_remote_job_read_state "$job" 2>/dev/null || true)
|
|
608
|
+
if [ "$state" = 'done' ]; then
|
|
609
|
+
fm_remote_job_reap "$account_home" "$id" 2>/dev/null || true
|
|
610
|
+
fi
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
fm_remote_job_stage() { # <account-home> <root> <home> <command> [args...]; stdin is captured
|
|
614
|
+
local account_home=$1 root=$2 home=$3 command=$4 stage id destination bytes queue_deadline owner_start
|
|
615
|
+
shift 4
|
|
616
|
+
fm_remote_job_prepare_state "$account_home" || return 1
|
|
617
|
+
root=$(fm_remote_job_canonical_existing_dir "$root") || {
|
|
618
|
+
FM_REMOTE_JOB_ERROR="remote job root is unavailable or unsafe"
|
|
619
|
+
return 1
|
|
620
|
+
}
|
|
621
|
+
home=$(fm_remote_job_canonical_home "$home") || {
|
|
622
|
+
FM_REMOTE_JOB_ERROR="remote job home is unavailable or unsafe"
|
|
623
|
+
return 1
|
|
624
|
+
}
|
|
625
|
+
case "$command" in fm-*.sh) ;; *) FM_REMOTE_JOB_ERROR="remote job command is outside the fm-*.sh namespace"; return 1 ;; esac
|
|
626
|
+
case "$command" in */*|*..*) FM_REMOTE_JOB_ERROR="remote job command contains a path or traversal"; return 1 ;; esac
|
|
627
|
+
owner_start=$(fm_remote_job_process_start "$$") || {
|
|
628
|
+
FM_REMOTE_JOB_ERROR="cannot establish remote job staging ownership"
|
|
629
|
+
return 1
|
|
630
|
+
}
|
|
631
|
+
stage=$(umask 077; mktemp -d "$FM_REMOTE_JOB_JOBS/.stage.XXXXXX") || {
|
|
632
|
+
FM_REMOTE_JOB_ERROR="cannot stage remote job"
|
|
633
|
+
return 1
|
|
634
|
+
}
|
|
635
|
+
chmod 700 "$stage" || { rm -rf -- "$stage"; return 1; }
|
|
636
|
+
queue_deadline=$(( $(date +%s) + FM_REMOTE_JOB_QUEUE_TIMEOUT ))
|
|
637
|
+
if ! printf '%s\n' "$$" > "$stage/.owner-pid" ||
|
|
638
|
+
! printf '%s\n' "$owner_start" > "$stage/.owner-start" ||
|
|
639
|
+
! chmod 600 "$stage/.owner-pid" "$stage/.owner-start" ||
|
|
640
|
+
! printf '%s\n' "$root" > "$stage/root" ||
|
|
641
|
+
! printf '%s\n' "$home" > "$stage/home" ||
|
|
642
|
+
! printf '%s\n' "$queue_deadline" > "$stage/queue_deadline" ||
|
|
643
|
+
! printf '%s\n' "$FM_REMOTE_JOB_TIMEOUT" > "$stage/timeout" ||
|
|
644
|
+
! printf '%s\0' "$command" "$@" > "$stage/argv" ||
|
|
645
|
+
! head -c "$((FM_REMOTE_JOB_MAX_BYTES + 1))" > "$stage/stdin"; then
|
|
646
|
+
rm -rf -- "$stage"
|
|
647
|
+
FM_REMOTE_JOB_ERROR="cannot capture remote job input"
|
|
648
|
+
return 1
|
|
649
|
+
fi
|
|
650
|
+
for bytes in root home queue_deadline timeout argv stdin; do chmod 600 "$stage/$bytes" || { rm -rf -- "$stage"; return 1; }; done
|
|
651
|
+
fm_remote_job_regular_bounded "$stage/argv" "$FM_REMOTE_JOB_MAX_BYTES" || {
|
|
652
|
+
rm -rf -- "$stage"
|
|
653
|
+
FM_REMOTE_JOB_ERROR="remote job argv exceeds the ${FM_REMOTE_JOB_MAX_BYTES}-byte bound"
|
|
654
|
+
return 1
|
|
655
|
+
}
|
|
656
|
+
fm_remote_job_regular_bounded "$stage/stdin" "$FM_REMOTE_JOB_MAX_BYTES" || {
|
|
657
|
+
rm -rf -- "$stage"
|
|
658
|
+
FM_REMOTE_JOB_ERROR="remote job stdin exceeds the ${FM_REMOTE_JOB_MAX_BYTES}-byte bound"
|
|
659
|
+
return 1
|
|
660
|
+
}
|
|
661
|
+
: > "$stage/stdout"
|
|
662
|
+
: > "$stage/stderr"
|
|
663
|
+
chmod 600 "$stage/stdout" "$stage/stderr" || { rm -rf -- "$stage"; return 1; }
|
|
664
|
+
id="job-${stage##*/.stage.}"
|
|
665
|
+
fm_remote_job_safe_id "$id" || { rm -rf -- "$stage"; return 1; }
|
|
666
|
+
destination="$FM_REMOTE_JOB_JOBS/$id"
|
|
667
|
+
[ ! -e "$destination" ] && [ ! -L "$destination" ] || { rm -rf -- "$stage"; return 1; }
|
|
668
|
+
if ! fm_remote_job_next_seq "$stage" "$destination" >/dev/null; then
|
|
669
|
+
rm -rf -- "$stage"
|
|
670
|
+
FM_REMOTE_JOB_ERROR="cannot allocate and publish a remote job staging sequence"
|
|
671
|
+
return 1
|
|
672
|
+
fi
|
|
673
|
+
# shellcheck disable=SC2034 # Sourceable API consumed by callers that do not use command substitution.
|
|
674
|
+
FM_REMOTE_JOB_ID=$id
|
|
675
|
+
printf '%s\n' "$id"
|
|
676
|
+
}
|
|
677
|
+
|
|
678
|
+
fm_remote_job_wait() { # <account-home> <id>; honors FM_REMOTE_JOB_DISCONNECT_PROBE
|
|
679
|
+
local account_home=$1 id=$2 job state queue_deadline execution_timeout wait_deadline exit_value
|
|
680
|
+
local now next_probe=0
|
|
681
|
+
fm_remote_job_prepare_state "$account_home" || return 1
|
|
682
|
+
job=$(fm_remote_job_job_dir "$id") || {
|
|
683
|
+
FM_REMOTE_JOB_ERROR="remote job record disappeared or became unsafe"
|
|
684
|
+
return 1
|
|
685
|
+
}
|
|
686
|
+
queue_deadline=$(fm_remote_job_read_number "$job" queue_deadline) || {
|
|
687
|
+
FM_REMOTE_JOB_ERROR="remote job queue deadline is invalid"
|
|
688
|
+
return 1
|
|
689
|
+
}
|
|
690
|
+
execution_timeout=$(fm_remote_job_read_number "$job" timeout) || {
|
|
691
|
+
FM_REMOTE_JOB_ERROR="remote job execution timeout is invalid"
|
|
692
|
+
return 1
|
|
693
|
+
}
|
|
694
|
+
[ "$execution_timeout" -le 3600 ] || {
|
|
695
|
+
FM_REMOTE_JOB_ERROR="remote job execution timeout is invalid"
|
|
696
|
+
return 1
|
|
697
|
+
}
|
|
698
|
+
wait_deadline=$((queue_deadline + execution_timeout + FM_REMOTE_JOB_WAIT_GRACE))
|
|
699
|
+
while :; do
|
|
700
|
+
state=$(fm_remote_job_read_state "$job" 2>/dev/null || true)
|
|
701
|
+
case "$state" in
|
|
702
|
+
'done')
|
|
703
|
+
if ! fm_remote_job_regular_bounded "$job/stdout" "$FM_REMOTE_JOB_MAX_BYTES" ||
|
|
704
|
+
! fm_remote_job_regular_bounded "$job/stderr" "$FM_REMOTE_JOB_MAX_BYTES" ||
|
|
705
|
+
! fm_remote_job_regular_bounded "$job/exit" 32; then
|
|
706
|
+
FM_REMOTE_JOB_ERROR="remote job result is unsafe or exceeds its byte bound"
|
|
707
|
+
return 1
|
|
708
|
+
fi
|
|
709
|
+
exit_value=$(tr -d '\n' < "$job/exit")
|
|
710
|
+
case "$exit_value" in ''|*[!0-9]*) FM_REMOTE_JOB_ERROR="remote job exit status is invalid"; return 1 ;; esac
|
|
711
|
+
[ "$exit_value" -le 255 ] || { FM_REMOTE_JOB_ERROR="remote job exit status is invalid"; return 1; }
|
|
712
|
+
# shellcheck disable=SC2034 # Sourceable API consumed by the entrypoint after this function returns.
|
|
713
|
+
FM_REMOTE_JOB_STDOUT="$job/stdout"
|
|
714
|
+
# shellcheck disable=SC2034 # Sourceable API consumed by the entrypoint after this function returns.
|
|
715
|
+
FM_REMOTE_JOB_STDERR="$job/stderr"
|
|
716
|
+
# shellcheck disable=SC2034 # Sourceable API consumed by the entrypoint after this function returns.
|
|
717
|
+
FM_REMOTE_JOB_EXIT=$exit_value
|
|
718
|
+
return 0
|
|
719
|
+
;;
|
|
720
|
+
queued|running) ;;
|
|
721
|
+
*) FM_REMOTE_JOB_ERROR="remote job state is invalid"; return 1 ;;
|
|
722
|
+
esac
|
|
723
|
+
now=$(date +%s)
|
|
724
|
+
if [ "$now" -ge "$wait_deadline" ]; then
|
|
725
|
+
FM_REMOTE_JOB_ERROR="remote job did not complete within its bounded wait"
|
|
726
|
+
return 1
|
|
727
|
+
fi
|
|
728
|
+
if [ -n "${FM_REMOTE_JOB_DISCONNECT_PROBE:-}" ] && [ "$now" -ge "$next_probe" ]; then
|
|
729
|
+
next_probe=$((now + 1))
|
|
730
|
+
if ! "$FM_REMOTE_JOB_DISCONNECT_PROBE"; then
|
|
731
|
+
fm_remote_job_cancel "$account_home" "$id" 2>/dev/null || true
|
|
732
|
+
FM_REMOTE_JOB_ERROR="remote job caller disconnected; the job was cancelled"
|
|
733
|
+
return 1
|
|
734
|
+
fi
|
|
735
|
+
fi
|
|
736
|
+
sleep "$FM_REMOTE_JOB_POLL_SECONDS"
|
|
737
|
+
done
|
|
738
|
+
}
|
|
739
|
+
|
|
740
|
+
fm_remote_job_reap() { # <account-home> <id>; only removes an exact completed record
|
|
741
|
+
local account_home=$1 id=$2 job file
|
|
742
|
+
fm_remote_job_prepare_state "$account_home" || return 1
|
|
743
|
+
job=$(fm_remote_job_job_dir "$id") || return 1
|
|
744
|
+
[ "$(fm_remote_job_read_state "$job")" = 'done' ] || return 1
|
|
745
|
+
for file in root home queue_deadline timeout deadline seq cancel argv stdin stdout stderr exit state .owner-pid .owner-start; do
|
|
746
|
+
[ -e "$job/$file" ] || continue
|
|
747
|
+
[ ! -L "$job/$file" ] || return 1
|
|
748
|
+
rm -f -- "$job/$file" || return 1
|
|
749
|
+
done
|
|
750
|
+
if [ -e "$job/.claim" ] || [ -L "$job/.claim" ]; then
|
|
751
|
+
[ -d "$job/.claim" ] && [ ! -L "$job/.claim" ] || return 1
|
|
752
|
+
fm_remote_job_remove_claim_records "$job/.claim" || return 1
|
|
753
|
+
rmdir "$job/.claim" || return 1
|
|
754
|
+
fi
|
|
755
|
+
rmdir "$job"
|
|
756
|
+
}
|
|
757
|
+
|
|
758
|
+
fm_remote_job_path_mtime() { # <path>
|
|
759
|
+
# The platform override controls worker shape in isolated tests, not the host
|
|
760
|
+
# kernel's stat syntax.
|
|
761
|
+
if [ "$(uname -s 2>/dev/null || true)" = Darwin ]; then stat -f %m "$1" 2>/dev/null; else stat -c %Y "$1" 2>/dev/null; fi
|
|
762
|
+
}
|
|
763
|
+
|
|
764
|
+
fm_remote_job_stage_owner_alive() { # <stage-dir>
|
|
765
|
+
local stage=$1 pid recorded_start actual_start
|
|
766
|
+
pid=$(fm_remote_job_read_single_line "$stage/.owner-pid" 64 2>/dev/null) || return 1
|
|
767
|
+
case "$pid" in ''|*[!0-9]*) return 1 ;; esac
|
|
768
|
+
[ "$pid" -gt 1 ] || return 1
|
|
769
|
+
recorded_start=$(fm_remote_job_read_single_line "$stage/.owner-start" 256 2>/dev/null) || return 1
|
|
770
|
+
actual_start=$(fm_remote_job_process_start "$pid" 2>/dev/null) || return 1
|
|
771
|
+
[ "$recorded_start" = "$actual_start" ]
|
|
772
|
+
}
|
|
773
|
+
|
|
774
|
+
fm_remote_job_reap_stale() { # <account-home>
|
|
775
|
+
local account_home=$1 job id state mtime now stage claim value marker tmp reap_claims=0
|
|
776
|
+
fm_remote_job_prepare_state "$account_home" || return 1
|
|
777
|
+
now=$(date +%s)
|
|
778
|
+
for job in "$FM_REMOTE_JOB_JOBS"/job-*; do
|
|
779
|
+
[ -d "$job" ] && [ ! -L "$job" ] || continue
|
|
780
|
+
id=${job##*/}
|
|
781
|
+
fm_remote_job_safe_id "$id" || continue
|
|
782
|
+
state=$(fm_remote_job_read_state "$job" 2>/dev/null || true)
|
|
783
|
+
[ "$state" = 'done' ] || continue
|
|
784
|
+
mtime=$(fm_remote_job_path_mtime "$job" 2>/dev/null || true)
|
|
785
|
+
case "$mtime" in ''|*[!0-9]*) continue ;; esac
|
|
786
|
+
[ $((now - mtime)) -ge "$FM_REMOTE_JOB_REAP_SECONDS" ] || continue
|
|
787
|
+
fm_remote_job_reap "$account_home" "$id" || true
|
|
788
|
+
done
|
|
789
|
+
marker="$FM_REMOTE_JOB_STATE/.seq-claims-reaped"
|
|
790
|
+
mtime=$(fm_remote_job_path_mtime "$marker" 2>/dev/null || true)
|
|
791
|
+
case "$mtime" in
|
|
792
|
+
''|*[!0-9]*) reap_claims=1 ;;
|
|
793
|
+
*) [ $((now - mtime)) -lt "$FM_REMOTE_JOB_SEQ_CLAIM_REAP_INTERVAL" ] || reap_claims=1 ;;
|
|
794
|
+
esac
|
|
795
|
+
if [ "$reap_claims" -eq 1 ]; then
|
|
796
|
+
tmp=$(umask 077; mktemp "$FM_REMOTE_JOB_STATE/.seqreap.XXXXXX") || tmp=
|
|
797
|
+
if [ -n "$tmp" ] && printf '%s\n' "$now" > "$tmp" && chmod 600 "$tmp" \
|
|
798
|
+
&& mv -f -- "$tmp" "$marker"; then
|
|
799
|
+
for claim in "$FM_REMOTE_JOB_SEQ_CLAIMS"/*; do
|
|
800
|
+
[ -d "$claim" ] && [ ! -L "$claim" ] || continue
|
|
801
|
+
value=${claim##*/}
|
|
802
|
+
case "$value" in ''|*[!0-9]*|0) continue ;; esac
|
|
803
|
+
mtime=$(fm_remote_job_path_mtime "$claim" 2>/dev/null || true)
|
|
804
|
+
case "$mtime" in ''|*[!0-9]*) continue ;; esac
|
|
805
|
+
[ $((now - mtime)) -ge "$FM_REMOTE_JOB_SEQ_CLAIM_REAP_SECONDS" ] || continue
|
|
806
|
+
rmdir "$claim" 2>/dev/null || true
|
|
807
|
+
done
|
|
808
|
+
else
|
|
809
|
+
[ -z "$tmp" ] || rm -f -- "$tmp"
|
|
810
|
+
fi
|
|
811
|
+
fi
|
|
812
|
+
# Staging litter a killed caller left behind is reaped after its owner is no
|
|
813
|
+
# longer the process that created it and the stage has exceeded the age bound.
|
|
814
|
+
for stage in "$FM_REMOTE_JOB_JOBS"/.stage.*; do
|
|
815
|
+
[ -d "$stage" ] && [ ! -L "$stage" ] || continue
|
|
816
|
+
fm_remote_job_stage_owner_alive "$stage" && continue
|
|
817
|
+
mtime=$(fm_remote_job_path_mtime "$stage" 2>/dev/null || true)
|
|
818
|
+
case "$mtime" in ''|*[!0-9]*) continue ;; esac
|
|
819
|
+
[ $((now - mtime)) -ge "$FM_REMOTE_JOB_STAGE_REAP_SECONDS" ] || continue
|
|
820
|
+
rm -rf -- "$stage"
|
|
821
|
+
done
|
|
822
|
+
}
|
|
823
|
+
|
|
824
|
+
fm_remote_job_launchagent_paths() { # <account-home>
|
|
825
|
+
local account_home=$1
|
|
826
|
+
FM_REMOTE_JOB_LAUNCH_AGENT_DIR="$account_home/Library/LaunchAgents"
|
|
827
|
+
FM_REMOTE_JOB_LAUNCH_AGENT_PLIST="$FM_REMOTE_JOB_LAUNCH_AGENT_DIR/$FM_REMOTE_JOB_LABEL.plist"
|
|
828
|
+
FM_REMOTE_JOB_LAUNCH_AGENT_LOG_DIR="$account_home/Library/Logs"
|
|
829
|
+
FM_REMOTE_JOB_LAUNCH_AGENT_LOG="$FM_REMOTE_JOB_LAUNCH_AGENT_LOG_DIR/$FM_REMOTE_JOB_LABEL.log"
|
|
830
|
+
}
|
|
831
|
+
|
|
832
|
+
fm_remote_job_plist_safe_path() {
|
|
833
|
+
case "$1" in *'&'*|*'<'*|*'>'*|*'"'*|*"'"*) return 1 ;; esac
|
|
834
|
+
}
|
|
835
|
+
|
|
836
|
+
fm_remote_job_render_launchagent() { # <remote-root> <account-home>
|
|
837
|
+
local root=$1 account_home=$2 worker
|
|
838
|
+
worker="$root/bin/fm-remote-job-worker.sh"
|
|
839
|
+
fm_remote_job_launchagent_paths "$account_home"
|
|
840
|
+
fm_remote_job_plist_safe_path "$worker" && fm_remote_job_plist_safe_path "$account_home" &&
|
|
841
|
+
fm_remote_job_plist_safe_path "$FM_REMOTE_JOB_LAUNCH_AGENT_LOG" || return 1
|
|
842
|
+
cat <<XML
|
|
843
|
+
<?xml version="1.0" encoding="UTF-8"?>
|
|
844
|
+
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
845
|
+
<plist version="1.0">
|
|
846
|
+
<dict>
|
|
847
|
+
<key>Label</key>
|
|
848
|
+
<string>$FM_REMOTE_JOB_LABEL</string>
|
|
849
|
+
<key>ProgramArguments</key>
|
|
850
|
+
<array>
|
|
851
|
+
<string>$worker</string>
|
|
852
|
+
</array>
|
|
853
|
+
<key>EnvironmentVariables</key>
|
|
854
|
+
<dict>
|
|
855
|
+
<key>HOME</key>
|
|
856
|
+
<string>$account_home</string>
|
|
857
|
+
<key>FM_ROOT_OVERRIDE</key>
|
|
858
|
+
<string>$root</string>
|
|
859
|
+
</dict>
|
|
860
|
+
<key>LimitLoadToSessionType</key>
|
|
861
|
+
<string>Aqua</string>
|
|
862
|
+
<key>RunAtLoad</key>
|
|
863
|
+
<true/>
|
|
864
|
+
<key>KeepAlive</key>
|
|
865
|
+
<true/>
|
|
866
|
+
<key>StandardOutPath</key>
|
|
867
|
+
<string>$FM_REMOTE_JOB_LAUNCH_AGENT_LOG</string>
|
|
868
|
+
<key>StandardErrorPath</key>
|
|
869
|
+
<string>$FM_REMOTE_JOB_LAUNCH_AGENT_LOG</string>
|
|
870
|
+
</dict>
|
|
871
|
+
</plist>
|
|
872
|
+
XML
|
|
873
|
+
}
|
|
874
|
+
|
|
875
|
+
fm_remote_job_launchagent_contract_matches() { # <remote-root> <account-home>
|
|
876
|
+
local root=$1 account_home=$2 actual expected
|
|
877
|
+
fm_remote_job_launchagent_paths "$account_home"
|
|
878
|
+
[ -f "$FM_REMOTE_JOB_LAUNCH_AGENT_PLIST" ] && [ ! -L "$FM_REMOTE_JOB_LAUNCH_AGENT_PLIST" ] || return 1
|
|
879
|
+
actual=$(tr -d ' \t\r\n' < "$FM_REMOTE_JOB_LAUNCH_AGENT_PLIST" 2>/dev/null) || return 1
|
|
880
|
+
expected=$(fm_remote_job_render_launchagent "$root" "$account_home" | tr -d ' \t\r\n') || return 1
|
|
881
|
+
[ "$actual" = "$expected" ]
|
|
882
|
+
}
|
|
883
|
+
|
|
884
|
+
fm_remote_job_gui_available() { # <uid>
|
|
885
|
+
local uid=$1
|
|
886
|
+
command -v launchctl >/dev/null 2>&1 && launchctl print "gui/$uid" >/dev/null 2>&1
|
|
887
|
+
}
|
|
888
|
+
|
|
889
|
+
fm_remote_job_launchagent_loaded() { # <remote-root> <account-home> <uid>
|
|
890
|
+
local root=$1 account_home=$2 uid=$3 worker loaded compact
|
|
891
|
+
fm_remote_job_launchagent_paths "$account_home"
|
|
892
|
+
worker="$root/bin/fm-remote-job-worker.sh"
|
|
893
|
+
loaded=$(launchctl print "gui/$uid/$FM_REMOTE_JOB_LABEL" 2>/dev/null) || return 1
|
|
894
|
+
compact=$(printf '%s' "$loaded" | tr -d ' \t\r\n') || return 1
|
|
895
|
+
[[ "$compact" == *"$FM_REMOTE_JOB_LABEL"* ]] || return 1
|
|
896
|
+
[[ "$compact" == *"$worker"* ]] || return 1
|
|
897
|
+
[[ "$compact" == *"$FM_REMOTE_JOB_LAUNCH_AGENT_PLIST"* ]] || return 1
|
|
898
|
+
}
|
|
899
|
+
|
|
900
|
+
fm_remote_job_worker_pid_path() { printf '%s\n' "$FM_REMOTE_JOB_STATE/worker.pid"; }
|
|
901
|
+
fm_remote_job_worker_ready_path() { printf '%s\n' "$FM_REMOTE_JOB_STATE/worker.ready"; }
|
|
902
|
+
fm_remote_job_worker_identity_path() { printf '%s\n' "$FM_REMOTE_JOB_STATE/worker.identity"; }
|
|
903
|
+
fm_remote_job_worker_lock_path() { printf '%s\n' "$FM_REMOTE_JOB_STATE/worker.lock"; }
|
|
904
|
+
|
|
905
|
+
fm_remote_job_process_start() {
|
|
906
|
+
local pid=$1 ps_bin value
|
|
907
|
+
if [ -x /bin/ps ]; then ps_bin=/bin/ps; elif [ -x /usr/bin/ps ]; then ps_bin=/usr/bin/ps; else return 1; fi
|
|
908
|
+
value=$("$ps_bin" -p "$pid" -o lstart= 2>/dev/null) || return 1
|
|
909
|
+
[ -n "$value" ] || return 1
|
|
910
|
+
case "$value" in *$'\n'*|*$'\r'*) return 1 ;; esac
|
|
911
|
+
printf '%s\n' "$value"
|
|
912
|
+
}
|
|
913
|
+
|
|
914
|
+
fm_remote_job_process_command() {
|
|
915
|
+
local pid=$1 ps_bin value
|
|
916
|
+
if [ -x /bin/ps ]; then ps_bin=/bin/ps; elif [ -x /usr/bin/ps ]; then ps_bin=/usr/bin/ps; else return 1; fi
|
|
917
|
+
value=$("$ps_bin" -p "$pid" -o command= 2>/dev/null) || return 1
|
|
918
|
+
[ -n "$value" ] || return 1
|
|
919
|
+
case "$value" in *$'\n'*|*$'\r'*) return 1 ;; esac
|
|
920
|
+
printf '%s\n' "$value"
|
|
921
|
+
}
|
|
922
|
+
|
|
923
|
+
fm_remote_job_process_pgid() { # <pid>
|
|
924
|
+
local pid=$1 ps_bin value
|
|
925
|
+
if [ -x /bin/ps ]; then ps_bin=/bin/ps; elif [ -x /usr/bin/ps ]; then ps_bin=/usr/bin/ps; else return 1; fi
|
|
926
|
+
value=$("$ps_bin" -p "$pid" -o pgid= 2>/dev/null) || return 1
|
|
927
|
+
value=$(printf '%s' "$value" | tr -d '[:space:]')
|
|
928
|
+
case "$value" in ''|*[!0-9]*) return 1 ;; esac
|
|
929
|
+
printf '%s\n' "$value"
|
|
930
|
+
}
|
|
931
|
+
|
|
932
|
+
# The code root a worker serves is still a genuine Firstmate checkout. A worker
|
|
933
|
+
# whose root fails this can never claim, validate, or execute another job, so
|
|
934
|
+
# the same predicate decides both self-termination and orphan reaping.
|
|
935
|
+
fm_remote_job_root_is_live() { # <remote-root>
|
|
936
|
+
local root=$1
|
|
937
|
+
[ -n "$root" ] || return 1
|
|
938
|
+
[ -d "$root" ] && [ ! -L "$root" ] || return 1
|
|
939
|
+
[ -f "$root/AGENTS.md" ] && [ ! -L "$root/AGENTS.md" ] || return 1
|
|
940
|
+
[ -f "$root/bin/fm-remote-job-worker.sh" ] && [ ! -L "$root/bin/fm-remote-job-worker.sh" ]
|
|
941
|
+
}
|
|
942
|
+
|
|
943
|
+
# The isolated process group that owns <pid>'s whole worker tree, echoed only
|
|
944
|
+
# when signalling it is provably safe: the group is not this shell's own, not a
|
|
945
|
+
# reserved id, and its leader is itself a remote job worker. A worker started
|
|
946
|
+
# without group isolation (an older build, or launchd's own session) therefore
|
|
947
|
+
# never yields a group, and callers fall back to signalling the single process.
|
|
948
|
+
fm_remote_job_worker_process_group() { # <pid>
|
|
949
|
+
local pid=$1 pgid own_pgid leader_command
|
|
950
|
+
pgid=$(fm_remote_job_process_pgid "$pid") || return 1
|
|
951
|
+
case "$pgid" in 0|1) return 1 ;; esac
|
|
952
|
+
own_pgid=$(fm_remote_job_process_pgid "$$") || return 1
|
|
953
|
+
[ "$pgid" != "$own_pgid" ] || return 1
|
|
954
|
+
leader_command=$(fm_remote_job_process_command "$pgid" 2>/dev/null || true)
|
|
955
|
+
case "$leader_command" in *fm-remote-job-worker.sh*) ;; *) return 1 ;; esac
|
|
956
|
+
printf '%s\n' "$pgid"
|
|
957
|
+
}
|
|
958
|
+
|
|
959
|
+
# Stop a worker and every descendant it leaked, TERM first and KILL only for a
|
|
960
|
+
# survivor. Signals the isolated worker group when one is provable and the lone
|
|
961
|
+
# process otherwise. Returns non-zero when any verified worker-group member is
|
|
962
|
+
# still alive afterwards.
|
|
963
|
+
fm_remote_job_stop_worker_tree() { # <pid>
|
|
964
|
+
local pid=$1 pgid i=0
|
|
965
|
+
case "$pid" in ''|*[!0-9]*) return 1 ;; esac
|
|
966
|
+
[ "$pid" -gt 1 ] || return 1
|
|
967
|
+
pgid=$(fm_remote_job_worker_process_group "$pid" 2>/dev/null || true)
|
|
968
|
+
if [ -n "$pgid" ]; then kill -TERM -- "-$pgid" 2>/dev/null || true; else kill -TERM "$pid" 2>/dev/null || true; fi
|
|
969
|
+
while { [ -n "$pgid" ] && kill -0 -- "-$pgid" 2>/dev/null || [ -z "$pgid" ] && kill -0 "$pid" 2>/dev/null; } \
|
|
970
|
+
&& [ "$i" -lt 50 ]; do
|
|
971
|
+
i=$((i + 1))
|
|
972
|
+
sleep 0.1
|
|
973
|
+
done
|
|
974
|
+
if [ -n "$pgid" ]; then
|
|
975
|
+
kill -0 -- "-$pgid" 2>/dev/null || return 0
|
|
976
|
+
else
|
|
977
|
+
kill -0 "$pid" 2>/dev/null || return 0
|
|
978
|
+
fi
|
|
979
|
+
if [ -n "$pgid" ]; then kill -KILL -- "-$pgid" 2>/dev/null || true; else kill -KILL "$pid" 2>/dev/null || true; fi
|
|
980
|
+
i=0
|
|
981
|
+
while { [ -n "$pgid" ] && kill -0 -- "-$pgid" 2>/dev/null || [ -z "$pgid" ] && kill -0 "$pid" 2>/dev/null; } \
|
|
982
|
+
&& [ "$i" -lt 50 ]; do
|
|
983
|
+
i=$((i + 1))
|
|
984
|
+
sleep 0.1
|
|
985
|
+
done
|
|
986
|
+
if [ -n "$pgid" ]; then
|
|
987
|
+
! kill -0 -- "-$pgid" 2>/dev/null
|
|
988
|
+
else
|
|
989
|
+
! kill -0 "$pid" 2>/dev/null
|
|
990
|
+
fi
|
|
991
|
+
}
|
|
992
|
+
|
|
993
|
+
fm_remote_job_read_single_line() {
|
|
994
|
+
local file=$1 max=$2 value extra
|
|
995
|
+
fm_remote_job_regular_bounded "$file" "$max" || return 1
|
|
996
|
+
IFS= read -r value < "$file" || return 1
|
|
997
|
+
if IFS= read -r extra < <(tail -n +2 "$file"); then
|
|
998
|
+
: "$extra"
|
|
999
|
+
return 1
|
|
1000
|
+
fi
|
|
1001
|
+
[ -n "$value" ] || return 1
|
|
1002
|
+
printf '%s\n' "$value"
|
|
1003
|
+
}
|
|
1004
|
+
|
|
1005
|
+
fm_remote_job_lock_owner_matches_process() {
|
|
1006
|
+
local account_home=$1 lock pid recorded_start actual_start recorded_command actual_command
|
|
1007
|
+
fm_remote_job_prepare_state "$account_home" || return 1
|
|
1008
|
+
lock=$(fm_remote_job_worker_lock_path)
|
|
1009
|
+
[ -d "$lock" ] && [ ! -L "$lock" ] || return 1
|
|
1010
|
+
pid=$(fm_remote_job_read_single_line "$lock/pid" 64) || return 1
|
|
1011
|
+
case "$pid" in ''|*[!0-9]*) return 1 ;; esac
|
|
1012
|
+
[ "$pid" -gt 1 ] || return 1
|
|
1013
|
+
recorded_start=$(fm_remote_job_read_single_line "$lock/start" 256) || return 1
|
|
1014
|
+
actual_start=$(fm_remote_job_process_start "$pid") || return 1
|
|
1015
|
+
[ "$recorded_start" = "$actual_start" ] || return 1
|
|
1016
|
+
recorded_command=$(fm_remote_job_read_single_line "$lock/command" 8192) || return 1
|
|
1017
|
+
actual_command=$(fm_remote_job_process_command "$pid") || return 1
|
|
1018
|
+
[ "$recorded_command" = "$actual_command" ] || return 1
|
|
1019
|
+
FM_REMOTE_JOB_OWNER_PID=$pid
|
|
1020
|
+
}
|
|
1021
|
+
|
|
1022
|
+
fm_remote_job_worker_owned_alive() {
|
|
1023
|
+
local root=$1 account_home=$2 lock pid pid_file identity_file command ps_bin
|
|
1024
|
+
[ "${FM_REMOTE_JOB_ACTIVE:-}" != 1 ] || return 0
|
|
1025
|
+
fm_remote_job_prepare_state "$account_home" || return 1
|
|
1026
|
+
lock=$(fm_remote_job_worker_lock_path)
|
|
1027
|
+
[ -d "$lock" ] && [ ! -L "$lock" ] || return 1
|
|
1028
|
+
[ ! -e "$lock/quarantine" ] && [ ! -L "$lock/quarantine" ] || return 1
|
|
1029
|
+
pid_file=$(fm_remote_job_worker_pid_path)
|
|
1030
|
+
pid=$(fm_remote_job_read_single_line "$pid_file" 64) || return 1
|
|
1031
|
+
case "$pid" in ''|*[!0-9]*) return 1 ;; esac
|
|
1032
|
+
identity_file=$(fm_remote_job_worker_identity_path)
|
|
1033
|
+
fm_remote_job_regular_bounded "$identity_file" 256 || return 1
|
|
1034
|
+
fm_remote_job_probe "$account_home" || return 1
|
|
1035
|
+
if fm_remote_job_lock_owner_matches_process "$account_home"; then
|
|
1036
|
+
[ "$pid" = "$FM_REMOTE_JOB_OWNER_PID" ] || return 1
|
|
1037
|
+
return 0
|
|
1038
|
+
fi
|
|
1039
|
+
[ ! -e "$lock/pid" ] && [ ! -L "$lock/pid" ] &&
|
|
1040
|
+
[ ! -e "$lock/start" ] && [ ! -L "$lock/start" ] &&
|
|
1041
|
+
[ ! -e "$lock/command" ] && [ ! -L "$lock/command" ] || return 1
|
|
1042
|
+
if [ -x /bin/ps ]; then ps_bin=/bin/ps; elif [ -x /usr/bin/ps ]; then ps_bin=/usr/bin/ps; else return 1; fi
|
|
1043
|
+
command=$("$ps_bin" -p "$pid" -o command= 2>/dev/null) || return 1
|
|
1044
|
+
case "$command" in *"$root/bin/fm-remote-job-worker.sh"*) FM_REMOTE_JOB_OWNER_PID=$pid; return 0 ;; esac
|
|
1045
|
+
return 1
|
|
1046
|
+
}
|
|
1047
|
+
|
|
1048
|
+
fm_remote_job_code_identity() { # <remote-root> <account-home>
|
|
1049
|
+
local root=$1 account_home=$2 git_bin root_hash library_hash worker_hash
|
|
1050
|
+
root=$(fm_remote_job_canonical_existing_dir "$root") || return 1
|
|
1051
|
+
[ -f "$root/bin/fm-remote-job-lib.sh" ] && [ ! -L "$root/bin/fm-remote-job-lib.sh" ] || return 1
|
|
1052
|
+
[ -f "$root/bin/fm-remote-job-worker.sh" ] && [ ! -L "$root/bin/fm-remote-job-worker.sh" ] || return 1
|
|
1053
|
+
fm_remote_job_compose_operator_path "$account_home" >/dev/null
|
|
1054
|
+
git_bin=$(fm_remote_job_operator_tool git 2>/dev/null || true)
|
|
1055
|
+
[ -n "$git_bin" ] || return 1
|
|
1056
|
+
root_hash=$(printf '%s' "$root" | "$git_bin" hash-object --stdin 2>/dev/null) || return 1
|
|
1057
|
+
library_hash=$("$git_bin" hash-object -- "$root/bin/fm-remote-job-lib.sh" 2>/dev/null) || return 1
|
|
1058
|
+
worker_hash=$("$git_bin" hash-object -- "$root/bin/fm-remote-job-worker.sh" 2>/dev/null) || return 1
|
|
1059
|
+
case "$root_hash:$library_hash:$worker_hash" in *[!0-9a-f:]*) return 1 ;; esac
|
|
1060
|
+
[ -n "$root_hash" ] && [ -n "$library_hash" ] && [ -n "$worker_hash" ] || return 1
|
|
1061
|
+
printf '%s:%s:%s\n' "$root_hash" "$library_hash" "$worker_hash"
|
|
1062
|
+
}
|
|
1063
|
+
|
|
1064
|
+
fm_remote_job_worker_identity_matches() { # <remote-root> <account-home>
|
|
1065
|
+
local root=$1 account_home=$2 identity_file expected actual extra
|
|
1066
|
+
[ "${FM_REMOTE_JOB_ACTIVE:-}" != 1 ] || return 0
|
|
1067
|
+
fm_remote_job_prepare_state "$account_home" || return 1
|
|
1068
|
+
identity_file=$(fm_remote_job_worker_identity_path)
|
|
1069
|
+
fm_remote_job_regular_bounded "$identity_file" 256 || return 1
|
|
1070
|
+
IFS= read -r actual < "$identity_file" || return 1
|
|
1071
|
+
if IFS= read -r extra < <(tail -n +2 "$identity_file"); then
|
|
1072
|
+
: "$extra"
|
|
1073
|
+
return 1
|
|
1074
|
+
fi
|
|
1075
|
+
expected=$(fm_remote_job_code_identity "$root" "$account_home") || return 1
|
|
1076
|
+
[ "$actual" = "$expected" ]
|
|
1077
|
+
}
|
|
1078
|
+
|
|
1079
|
+
fm_remote_job_worker_alive() { # <account-home>
|
|
1080
|
+
local account_home=$1 pid
|
|
1081
|
+
fm_remote_job_prepare_state "$account_home" || return 1
|
|
1082
|
+
pid=$(cat "$(fm_remote_job_worker_pid_path)" 2>/dev/null || true)
|
|
1083
|
+
case "$pid" in ''|*[!0-9]*) return 1 ;; esac
|
|
1084
|
+
kill -0 "$pid" 2>/dev/null
|
|
1085
|
+
}
|
|
1086
|
+
|
|
1087
|
+
fm_remote_job_probe() { # <account-home>; a fresh worker heartbeat or active job proves readiness
|
|
1088
|
+
local account_home=$1 ready lock mtime now
|
|
1089
|
+
[ "${FM_REMOTE_JOB_ACTIVE:-}" = 1 ] && return 0
|
|
1090
|
+
fm_remote_job_prepare_state "$account_home" || return 1
|
|
1091
|
+
lock=$(fm_remote_job_worker_lock_path)
|
|
1092
|
+
[ ! -e "$lock/quarantine" ] && [ ! -L "$lock/quarantine" ] || return 1
|
|
1093
|
+
ready=$(fm_remote_job_worker_ready_path)
|
|
1094
|
+
[ -f "$ready" ] && [ ! -L "$ready" ] || return 1
|
|
1095
|
+
mtime=$(fm_remote_job_path_mtime "$ready" 2>/dev/null || true)
|
|
1096
|
+
case "$mtime" in ''|*[!0-9]*) return 1 ;; esac
|
|
1097
|
+
now=$(date +%s)
|
|
1098
|
+
[ $((now - mtime)) -le 10 ]
|
|
1099
|
+
}
|
|
1100
|
+
|
|
1101
|
+
fm_remote_job_wait_for_probe() { # <remote-root> <account-home>
|
|
1102
|
+
local root=$1 account_home=$2 i=0
|
|
1103
|
+
while [ "$i" -lt 200 ]; do
|
|
1104
|
+
fm_remote_job_probe "$account_home" && fm_remote_job_worker_identity_matches "$root" "$account_home" && return 0
|
|
1105
|
+
i=$((i + 1))
|
|
1106
|
+
sleep 0.1
|
|
1107
|
+
done
|
|
1108
|
+
return 1
|
|
1109
|
+
}
|
|
1110
|
+
|
|
1111
|
+
fm_remote_job_write_launchagent() { # <remote-root> <account-home>
|
|
1112
|
+
local root=$1 account_home=$2 tmp
|
|
1113
|
+
fm_remote_job_launchagent_paths "$account_home"
|
|
1114
|
+
if ! mkdir -p "$FM_REMOTE_JOB_LAUNCH_AGENT_DIR" 2>/dev/null ||
|
|
1115
|
+
! mkdir -p "$FM_REMOTE_JOB_LAUNCH_AGENT_LOG_DIR" 2>/dev/null; then
|
|
1116
|
+
FM_REMOTE_JOB_ERROR="cannot create the remote job LaunchAgent directories"
|
|
1117
|
+
return 1
|
|
1118
|
+
fi
|
|
1119
|
+
[ -d "$FM_REMOTE_JOB_LAUNCH_AGENT_DIR" ] && [ ! -L "$FM_REMOTE_JOB_LAUNCH_AGENT_DIR" ] || return 1
|
|
1120
|
+
[ -d "$FM_REMOTE_JOB_LAUNCH_AGENT_LOG_DIR" ] && [ ! -L "$FM_REMOTE_JOB_LAUNCH_AGENT_LOG_DIR" ] || return 1
|
|
1121
|
+
tmp="$FM_REMOTE_JOB_LAUNCH_AGENT_DIR/.$FM_REMOTE_JOB_LABEL.plist.tmp.$$"
|
|
1122
|
+
fm_remote_job_render_launchagent "$root" "$account_home" > "$tmp" || {
|
|
1123
|
+
rm -f -- "$tmp"
|
|
1124
|
+
FM_REMOTE_JOB_ERROR="remote job paths cannot be embedded safely in a property list"
|
|
1125
|
+
return 1
|
|
1126
|
+
}
|
|
1127
|
+
chmod 0644 "$tmp" 2>/dev/null || true
|
|
1128
|
+
mv -f -- "$tmp" "$FM_REMOTE_JOB_LAUNCH_AGENT_PLIST" || {
|
|
1129
|
+
rm -f -- "$tmp"
|
|
1130
|
+
FM_REMOTE_JOB_ERROR="cannot publish $FM_REMOTE_JOB_LAUNCH_AGENT_PLIST"
|
|
1131
|
+
return 1
|
|
1132
|
+
}
|
|
1133
|
+
}
|
|
1134
|
+
|
|
1135
|
+
fm_remote_job_reload_launchagent() { # <account-home> <uid>
|
|
1136
|
+
local account_home=$1 uid=$2 out
|
|
1137
|
+
fm_remote_job_launchagent_paths "$account_home"
|
|
1138
|
+
launchctl bootout "gui/$uid/$FM_REMOTE_JOB_LABEL" >/dev/null 2>&1 || true
|
|
1139
|
+
if ! out=$(launchctl bootstrap "gui/$uid" "$FM_REMOTE_JOB_LAUNCH_AGENT_PLIST" 2>&1); then
|
|
1140
|
+
FM_REMOTE_JOB_ERROR="launchctl bootstrap gui/$uid refused: ${out:-no diagnostic}"
|
|
1141
|
+
return 1
|
|
1142
|
+
fi
|
|
1143
|
+
if ! out=$(launchctl kickstart -k "gui/$uid/$FM_REMOTE_JOB_LABEL" 2>&1); then
|
|
1144
|
+
FM_REMOTE_JOB_ERROR="launchctl kickstart gui/$uid/$FM_REMOTE_JOB_LABEL refused: ${out:-no diagnostic}"
|
|
1145
|
+
return 1
|
|
1146
|
+
fi
|
|
1147
|
+
}
|
|
1148
|
+
|
|
1149
|
+
fm_remote_job_start_linux_worker() { # <remote-root> <account-home>
|
|
1150
|
+
local root=$1 account_home=$2 worker pid
|
|
1151
|
+
worker="$root/bin/fm-remote-job-worker.sh"
|
|
1152
|
+
[ -f "$worker" ] && [ ! -L "$worker" ] && [ -x "$worker" ] || {
|
|
1153
|
+
FM_REMOTE_JOB_ERROR="remote job worker is not a genuine executable in the configured code root"
|
|
1154
|
+
return 1
|
|
1155
|
+
}
|
|
1156
|
+
fm_remote_job_prepare_state "$account_home" || return 1
|
|
1157
|
+
if fm_remote_job_worker_owned_alive "$root" "$account_home"; then
|
|
1158
|
+
if fm_remote_job_worker_identity_matches "$root" "$account_home"; then return 0; fi
|
|
1159
|
+
# The owner pid is the serving child; its restart supervisor sits above it
|
|
1160
|
+
# and would immediately replace a lone process kill, so stop the whole
|
|
1161
|
+
# worker tree through its isolated group.
|
|
1162
|
+
pid=$FM_REMOTE_JOB_OWNER_PID
|
|
1163
|
+
fm_remote_job_stop_worker_tree "$pid" || {
|
|
1164
|
+
FM_REMOTE_JOB_ERROR="stale remote job worker did not stop safely"
|
|
1165
|
+
return 1
|
|
1166
|
+
}
|
|
1167
|
+
wait "$pid" 2>/dev/null || true
|
|
1168
|
+
FM_REMOTE_JOB_REPAIRED=1
|
|
1169
|
+
fi
|
|
1170
|
+
# Job control puts the worker tree in its own process group, so a later stop
|
|
1171
|
+
# can signal every descendant at once without ever reaching the caller's own
|
|
1172
|
+
# group. Without this the group of a leaked worker is the launching command's.
|
|
1173
|
+
set -m
|
|
1174
|
+
nohup env \
|
|
1175
|
+
HOME="$account_home" \
|
|
1176
|
+
FM_ROOT_OVERRIDE="$root" \
|
|
1177
|
+
FM_REMOTE_JOB_STATE_ROOT="$FM_REMOTE_JOB_STATE" \
|
|
1178
|
+
FM_REMOTE_JOB_PLATFORM_OVERRIDE="${FM_REMOTE_JOB_PLATFORM_OVERRIDE:-}" \
|
|
1179
|
+
"$worker" >> "$FM_REMOTE_JOB_STATE/logs/$FM_REMOTE_JOB_LABEL.log" 2>&1 < /dev/null &
|
|
1180
|
+
pid=$!
|
|
1181
|
+
set +m
|
|
1182
|
+
case "$pid" in ''|*[!0-9]*) FM_REMOTE_JOB_ERROR="could not start the remote job worker"; return 1 ;; esac
|
|
1183
|
+
FM_REMOTE_JOB_REPAIRED=1
|
|
1184
|
+
}
|
|
1185
|
+
|
|
1186
|
+
fm_remote_job_ensure_worker() { # <remote-root> <account-home>
|
|
1187
|
+
local root=$1 account_home=$2 platform uid identity_matches=0
|
|
1188
|
+
FM_REMOTE_JOB_ERROR=
|
|
1189
|
+
FM_REMOTE_JOB_REPAIRED=0
|
|
1190
|
+
root=$(fm_remote_job_canonical_existing_dir "$root") || {
|
|
1191
|
+
FM_REMOTE_JOB_ERROR="configured remote root is unavailable or unsafe"
|
|
1192
|
+
return 1
|
|
1193
|
+
}
|
|
1194
|
+
account_home=$(fm_remote_job_canonical_existing_dir "$account_home") || {
|
|
1195
|
+
FM_REMOTE_JOB_ERROR="remote account home is unavailable or unsafe"
|
|
1196
|
+
return 1
|
|
1197
|
+
}
|
|
1198
|
+
[ -f "$root/bin/fm-remote-job-worker.sh" ] && [ ! -L "$root/bin/fm-remote-job-worker.sh" ] &&
|
|
1199
|
+
[ -x "$root/bin/fm-remote-job-worker.sh" ] || {
|
|
1200
|
+
FM_REMOTE_JOB_ERROR="configured remote root has no safe executable remote job worker"
|
|
1201
|
+
return 1
|
|
1202
|
+
}
|
|
1203
|
+
platform=$(fm_remote_job_platform)
|
|
1204
|
+
fm_remote_job_worker_identity_matches "$root" "$account_home" && identity_matches=1
|
|
1205
|
+
if [ "$platform" = darwin ]; then
|
|
1206
|
+
uid=$(id -u 2>/dev/null || true)
|
|
1207
|
+
case "$uid" in ''|*[!0-9]*) FM_REMOTE_JOB_ERROR="remote account uid is unavailable; run fm-on.sh <route> fm-remote-doctor.sh --fix"; return 1 ;; esac
|
|
1208
|
+
if ! fm_remote_job_gui_available "$uid"; then
|
|
1209
|
+
FM_REMOTE_JOB_ERROR="no Aqua login session exists for uid $uid; log that account in at the console, then run fm-on.sh <route> fm-remote-doctor.sh --fix"
|
|
1210
|
+
return 1
|
|
1211
|
+
fi
|
|
1212
|
+
if ! fm_remote_job_launchagent_contract_matches "$root" "$account_home"; then
|
|
1213
|
+
fm_remote_job_write_launchagent "$root" "$account_home" || return 1
|
|
1214
|
+
FM_REMOTE_JOB_REPAIRED=1
|
|
1215
|
+
fi
|
|
1216
|
+
if ! fm_remote_job_launchagent_loaded "$root" "$account_home" "$uid" ||
|
|
1217
|
+
[ "$FM_REMOTE_JOB_REPAIRED" -eq 1 ] || [ "$identity_matches" -eq 0 ]; then
|
|
1218
|
+
fm_remote_job_reload_launchagent "$account_home" "$uid" || return 1
|
|
1219
|
+
FM_REMOTE_JOB_REPAIRED=1
|
|
1220
|
+
fi
|
|
1221
|
+
else
|
|
1222
|
+
fm_remote_job_start_linux_worker "$root" "$account_home" || return 1
|
|
1223
|
+
fi
|
|
1224
|
+
fm_remote_job_wait_for_probe "$root" "$account_home" && return 0
|
|
1225
|
+
if [ "$platform" = darwin ]; then
|
|
1226
|
+
fm_remote_job_reload_launchagent "$account_home" "$uid" || return 1
|
|
1227
|
+
FM_REMOTE_JOB_REPAIRED=1
|
|
1228
|
+
fm_remote_job_wait_for_probe "$root" "$account_home" && return 0
|
|
1229
|
+
else
|
|
1230
|
+
# A replaced Linux supervisor can lose its first ownership race while the
|
|
1231
|
+
# prior supervisor finishes releasing the shared worker lock. Retry the
|
|
1232
|
+
# idempotent start once, matching the bounded recovery already used above
|
|
1233
|
+
# for launchd, before reporting a startup failure.
|
|
1234
|
+
fm_remote_job_start_linux_worker "$root" "$account_home" || return 1
|
|
1235
|
+
FM_REMOTE_JOB_REPAIRED=1
|
|
1236
|
+
fm_remote_job_wait_for_probe "$root" "$account_home" && return 0
|
|
1237
|
+
fi
|
|
1238
|
+
# shellcheck disable=SC2034 # Sourceable API consumed by the entrypoint and remote doctor.
|
|
1239
|
+
FM_REMOTE_JOB_ERROR="remote job worker did not report ready after startup"
|
|
1240
|
+
return 1
|
|
1241
|
+
}
|