Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

plugin: add library support for using and providing plugins #485

Open
FiloSottile opened this issue Jan 12, 2023 Discussed in #480 · 4 comments
Open

plugin: add library support for using and providing plugins #485

FiloSottile opened this issue Jan 12, 2023 Discussed in #480 · 4 comments

Comments

@FiloSottile
Copy link
Owner

Also discussed in #480.

@FiloSottile
Copy link
Owner Author

See the plugin package as exposed in https://pkg.go.dev/filippo.io/age/[email protected].

@ngortheone
Copy link

@FiloSottile the link appears to be broken

I am trying to use age programmatically and I have AGE-PLUGIN-... identity

@quite
Copy link

quite commented Mar 11, 2024

@ngortheone the plugin package is available on main by now, since last autumn I believe. But not tagged yet. I'm using it in https://github.com/quite/age-plugin-tkey

@AnomalRoil
Copy link

@FiloSottile I have actually started working on plage which would be a "plugin for age" Go library, solving this issue. But I'm not too happy with my current design, especially for the API around the bidirectional Phase 2 parts.

I'd love to discuss designs options further, tho.
Phase 1 being unidirectional, it's easy to provide a library with an API for it.

My current design:

  1. Users (plugin creators) can instantiate a plugin, by providing its name and 2 functions as input: a newRec func() RecipientV1 and a newId func() IdentityV1, notice how I currently didn't decide to use the plugin.Recipient type here since it would require the user of the plugin library to specify their own ClientUI and all, but these seemed more of a concern for the age implementation calling the plugin rather than a concern that all plugin creators should be caring about. So RecipientV1 and IdentityV1 are currently interfaces of the form:

    type RecipientV1 interface {
        age.Recipient
        SetRecipient([]byte) error
    }
    

    (which is arguably not the best kind of Go API, but feels like the easiest way of letting plugin creators define their own types and then passing them to the plage plugin library to handle.)

    The idea of having a plugin object is that it can hold the state, and the details about the plugin.

    This has the advantage of meaning plugin creators mostly just need to create a Wrap and a Unwrap function on their very own recipient and identity types, and a Set([]byte) function without having to care about formatting, stanzas, bech32, etc.

  2. Users can also play with the stdin, stdout and stderr pipes (mostly in order to enable easy testing of plugins) used by a plugin after having instantiated one.

  3. Users can then p.RunPhase1() on their newly instantiated p plugin, which will parse and use the required (state machine) flag for an age-plugin, using the phase 1 state machine to populate its internal state. This is easy because it's being driven by age and is unidirectional. So in theory no need for callbacks or user interactions in this step.

  4. Now comes the tricky bits: Phase 2 is driven by the plugin and is bi-directional. One one side, I could just provide all the required bits like "CommandReader", "CommandWriter" structs, "NewCommand" functions for the various functions supported by the age-plugin spec, and expect the plugin creator to handle the entire thing themselves and basically let the implementation of most of the state machine for Phase 2 to users... But this sounds painful to use.

    On the other hand, if I provide a RunPhase2 function, suddenly all the possible user interactions in that phase become painful and the current "best solution" I could think of would be to provide callbacks to the plugin creators that could be run in certain message types, but this feels brittle at best.
    But this might be because I'm not tying the callbacks properly to the rights things. Maybe just having a way of tying callbacks to identity and recipients and filekeys would be enough, but it does feel not too comfy to use neither.

    So I'd love to hear your opinion on how best to support plugin creators in Phase 2 without delegating all of the work to them.

I guess another open question would be key-generation, should I let plugin creators handle that or should the NewPlugin also expect a GenerateIdentity() IdentityV1 function and parse the --generate-key flag or something like that. Notice the current plugin specs just says:

It is expected that the same plugin binary will be used (potentially with other argument flags) for administrative tasks like generating keys.

so I'm hesitant to provide a "canonical" way of doing key gen, but it does feel like a desirable feature of a plugin library. WDYT?

Finally, re. the current age/plugin package, I think it'd be great if you could also expose the StanzaReader that is in internal/format/format.go since it is required to parse commands and messages that age expects.
Do you have a strong reason not to expose it currently?

FiloSottile added a commit that referenced this issue Jun 17, 2024
FiloSottile added a commit that referenced this issue Jun 17, 2024
FiloSottile added a commit that referenced this issue Jun 18, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

4 participants